mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2024-11-16 14:54:54 +00:00
Merge branch 'mc-1.18.x' into mc-1.19.x
This commit is contained in:
commit
629abb65e3
@ -86,11 +86,17 @@ minecraft {
|
|||||||
val testMod = configurations["testModRuntimeClasspath"].resolve()
|
val testMod = configurations["testModRuntimeClasspath"].resolve()
|
||||||
val implementation = configurations.runtimeClasspath.get().resolve()
|
val implementation = configurations.runtimeClasspath.get().resolve()
|
||||||
val new = (testMod - implementation)
|
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)
|
.joinToString(File.pathSeparator)
|
||||||
if (old == null) new else old.get() + File.pathSeparator + new
|
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") {
|
mods.register("cctest") {
|
||||||
source(sourceSets["testMod"])
|
source(sourceSets["testMod"])
|
||||||
source(sourceSets["testFixtures"])
|
source(sourceSets["testFixtures"])
|
||||||
@ -114,7 +120,6 @@ minecraft {
|
|||||||
mappings("parchment", "${libs.versions.parchmentMc.get()}-${libs.versions.parchment.get()}-$mcVersion")
|
mappings("parchment", "${libs.versions.parchmentMc.get()}-${libs.versions.parchment.get()}-$mcVersion")
|
||||||
|
|
||||||
accessTransformer(file("src/main/resources/META-INF/accesstransformer.cfg"))
|
accessTransformer(file("src/main/resources/META-INF/accesstransformer.cfg"))
|
||||||
accessTransformer(file("src/testMod/resources/META-INF/accesstransformer.cfg"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mixin {
|
mixin {
|
||||||
|
@ -78,7 +78,9 @@
|
|||||||
|
|
||||||
<!-- Javadoc -->
|
<!-- Javadoc -->
|
||||||
<!-- TODO: Missing* checks for the dan200.computercraft.api package? -->
|
<!-- 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="InvalidJavadocPosition" />
|
||||||
<module name="JavadocBlockTagLocation" />
|
<module name="JavadocBlockTagLocation" />
|
||||||
<module name="JavadocMethod"/>
|
<module name="JavadocMethod"/>
|
||||||
@ -107,7 +109,9 @@
|
|||||||
<module name="LocalFinalVariableName" />
|
<module name="LocalFinalVariableName" />
|
||||||
<module name="LocalVariableName" />
|
<module name="LocalVariableName" />
|
||||||
<module name="MemberName" />
|
<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="MethodTypeParameterName" />
|
||||||
<module name="PackageName">
|
<module name="PackageName">
|
||||||
<property name="format" value="^dan200\.computercraft(\.[a-z][a-z0-9]*)*" />
|
<property name="format" value="^dan200\.computercraft(\.[a-z][a-z0-9]*)*" />
|
||||||
@ -115,11 +119,6 @@
|
|||||||
<module name="ParameterName" />
|
<module name="ParameterName" />
|
||||||
<module name="StaticVariableName">
|
<module name="StaticVariableName">
|
||||||
<property name="format" value="^[a-z][a-zA-Z0-9]*|CAPABILITY(_[A-Z_]+)?$" />
|
<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>
|
||||||
<module name="TypeName" />
|
<module name="TypeName" />
|
||||||
|
|
||||||
|
@ -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
|
completion candidate). `include_dirs` can be set to @{false} to only include
|
||||||
those with a trailing slash.
|
those with a trailing slash.
|
||||||
|
|
||||||
@tparam string path The path to complete.
|
@tparam[1] string path The path to complete.
|
||||||
@tparam string location The location where paths are resolved from.
|
@tparam[1] string location The location where paths are resolved from.
|
||||||
@tparam[opt] boolean include_files When @{false}, only directories will be
|
@tparam[1,opt=true] boolean include_files When @{false}, only directories will
|
||||||
included in the returned list.
|
be included in the returned list.
|
||||||
@tparam[opt] boolean include_dirs When @{false}, "raw" directories will not be
|
@tparam[1,opt=true] boolean include_dirs When @{false}, "raw" directories will
|
||||||
included in the returned list.
|
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.
|
@treturn { string... } A list of possible completion candidates.
|
||||||
@since 1.74
|
@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
|
function complete(path, location, include_files, include_dirs) end
|
||||||
|
@ -5,7 +5,7 @@ kotlin.stdlib.default.dependency=false
|
|||||||
kotlin.jvm.target.validation.mode=error
|
kotlin.jvm.target.validation.mode=error
|
||||||
|
|
||||||
# Mod properties
|
# 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
|
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
|
||||||
mcVersion=1.19.2
|
mcVersion=1.19.2
|
||||||
|
@ -7,7 +7,7 @@ parchment = "2022.10.16"
|
|||||||
parchmentMc = "1.19.2"
|
parchmentMc = "1.19.2"
|
||||||
|
|
||||||
autoService = "1.0.1"
|
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"
|
jetbrainsAnnotations = "23.0.0"
|
||||||
kotlin = "1.7.10"
|
kotlin = "1.7.10"
|
||||||
kotlin-coroutines = "1.6.0"
|
kotlin-coroutines = "1.6.0"
|
||||||
|
@ -15,6 +15,7 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.OptionalInt;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@Mod( ComputerCraft.MOD_ID )
|
@Mod( ComputerCraft.MOD_ID )
|
||||||
@ -37,8 +38,8 @@ public final class ComputerCraft
|
|||||||
public static boolean httpEnabled = true;
|
public static boolean httpEnabled = true;
|
||||||
public static boolean httpWebsocketEnabled = true;
|
public static boolean httpWebsocketEnabled = true;
|
||||||
public static List<AddressRule> httpRules = List.of(
|
public static List<AddressRule> httpRules = List.of(
|
||||||
AddressRule.parse( "$private", null, Action.DENY.toPartial() ),
|
AddressRule.parse( "$private", OptionalInt.empty(), Action.DENY.toPartial() ),
|
||||||
AddressRule.parse( "*", null, Action.ALLOW.toPartial() )
|
AddressRule.parse( "*", OptionalInt.empty(), Action.ALLOW.toPartial() )
|
||||||
);
|
);
|
||||||
|
|
||||||
public static int httpMaxRequests = 16;
|
public static int httpMaxRequests = 16;
|
||||||
|
@ -22,7 +22,7 @@ import dan200.computercraft.api.redstone.IBundledRedstoneProvider;
|
|||||||
import dan200.computercraft.core.apis.ApiFactories;
|
import dan200.computercraft.core.apis.ApiFactories;
|
||||||
import dan200.computercraft.core.asm.GenericMethod;
|
import dan200.computercraft.core.asm.GenericMethod;
|
||||||
import dan200.computercraft.core.filesystem.FileMount;
|
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.ComputerCraftAPIService;
|
||||||
import dan200.computercraft.impl.detail.DetailRegistryImpl;
|
import dan200.computercraft.impl.detail.DetailRegistryImpl;
|
||||||
import dan200.computercraft.shared.BundledRedstone;
|
import dan200.computercraft.shared.BundledRedstone;
|
||||||
|
@ -16,6 +16,7 @@ import net.minecraft.network.chat.Component;
|
|||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.function.BooleanSupplier;
|
import java.util.function.BooleanSupplier;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@ -56,9 +57,8 @@ public final class ComputerSidebar
|
|||||||
() -> isOn.getAsBoolean() ? Arrays.asList(
|
() -> isOn.getAsBoolean() ? Arrays.asList(
|
||||||
Component.translatable( "gui.computercraft.tooltip.turn_off" ),
|
Component.translatable( "gui.computercraft.tooltip.turn_off" ),
|
||||||
Component.translatable( "gui.computercraft.tooltip.turn_off.key" ).withStyle( ChatFormatting.GRAY )
|
Component.translatable( "gui.computercraft.tooltip.turn_off.key" ).withStyle( ChatFormatting.GRAY )
|
||||||
) : Arrays.asList(
|
) : Collections.singletonList(
|
||||||
Component.translatable( "gui.computercraft.tooltip.turn_on" ),
|
Component.translatable( "gui.computercraft.tooltip.turn_on" )
|
||||||
Component.translatable( "gui.computercraft.tooltip.turn_off.key" ).withStyle( ChatFormatting.GRAY )
|
|
||||||
)
|
)
|
||||||
) );
|
) );
|
||||||
|
|
||||||
|
@ -8,7 +8,8 @@ package dan200.computercraft.client.pocket;
|
|||||||
import dan200.computercraft.ComputerCraft;
|
import dan200.computercraft.ComputerCraft;
|
||||||
import dan200.computercraft.core.terminal.Terminal;
|
import dan200.computercraft.core.terminal.Terminal;
|
||||||
import dan200.computercraft.shared.computer.core.ComputerState;
|
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 dan200.computercraft.shared.pocket.core.PocketServerComputer;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
@ -25,13 +26,13 @@ import javax.annotation.Nonnull;
|
|||||||
*/
|
*/
|
||||||
public class PocketComputerData
|
public class PocketComputerData
|
||||||
{
|
{
|
||||||
private final Terminal terminal;
|
private final NetworkedTerminal terminal;
|
||||||
private ComputerState state = ComputerState.OFF;
|
private ComputerState state = ComputerState.OFF;
|
||||||
private int lightColour = -1;
|
private int lightColour = -1;
|
||||||
|
|
||||||
public PocketComputerData( boolean colour )
|
public PocketComputerData( boolean colour )
|
||||||
{
|
{
|
||||||
terminal = new Terminal( ComputerCraft.pocketTermWidth, ComputerCraft.pocketTermHeight, colour );
|
terminal = new NetworkedTerminal( ComputerCraft.pocketTermWidth, ComputerCraft.pocketTermHeight, colour );
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getLightState()
|
public int getLightState()
|
||||||
|
@ -6,13 +6,17 @@
|
|||||||
package dan200.computercraft.core.apis.http.options;
|
package dan200.computercraft.core.apis.http.options;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
import java.util.OptionalInt;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
|
||||||
public enum Action
|
public enum Action
|
||||||
{
|
{
|
||||||
ALLOW,
|
ALLOW,
|
||||||
DENY;
|
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
|
@Nonnull
|
||||||
public PartialOptions toPartial()
|
public PartialOptions toPartial()
|
||||||
|
@ -16,6 +16,7 @@ import java.net.Inet4Address;
|
|||||||
import java.net.Inet6Address;
|
import java.net.Inet6Address;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
|
import java.util.OptionalInt;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -29,10 +30,10 @@ public final class AddressRule
|
|||||||
public static final int WEBSOCKET_MESSAGE = 128 * 1024;
|
public static final int WEBSOCKET_MESSAGE = 128 * 1024;
|
||||||
|
|
||||||
private final AddressPredicate predicate;
|
private final AddressPredicate predicate;
|
||||||
private final Integer port;
|
private final OptionalInt port;
|
||||||
private final PartialOptions partial;
|
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.predicate = predicate;
|
||||||
this.partial = partial;
|
this.partial = partial;
|
||||||
@ -40,7 +41,7 @@ public final class AddressRule
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@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( '/' );
|
int cidr = filter.indexOf( '/' );
|
||||||
if( cidr >= 0 )
|
if( cidr >= 0 )
|
||||||
@ -72,7 +73,7 @@ public final class AddressRule
|
|||||||
*/
|
*/
|
||||||
private boolean matches( String domain, int port, InetAddress address, Inet4Address ipv4Address )
|
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 )
|
return predicate.matches( domain )
|
||||||
|| predicate.matches( address )
|
|| predicate.matches( address )
|
||||||
|| (ipv4Address != null && predicate.matches( ipv4Address ));
|
|| (ipv4Address != null && predicate.matches( ipv4Address ));
|
||||||
@ -80,8 +81,7 @@ public final class AddressRule
|
|||||||
|
|
||||||
public static Options apply( Iterable<? extends AddressRule> rules, String domain, InetSocketAddress socketAddress )
|
public static Options apply( Iterable<? extends AddressRule> rules, String domain, InetSocketAddress socketAddress )
|
||||||
{
|
{
|
||||||
PartialOptions options = null;
|
PartialOptions options = PartialOptions.DEFAULT;
|
||||||
boolean hasMany = false;
|
|
||||||
|
|
||||||
int port = socketAddress.getPort();
|
int port = socketAddress.getPort();
|
||||||
InetAddress address = socketAddress.getAddress();
|
InetAddress address = socketAddress.getAddress();
|
||||||
@ -91,24 +91,9 @@ public final class AddressRule
|
|||||||
for( AddressRule rule : rules )
|
for( AddressRule rule : rules )
|
||||||
{
|
{
|
||||||
if( !rule.matches( domain, port, address, ipv4Address ) ) continue;
|
if( !rule.matches( domain, port, address, ipv4Address ) ) continue;
|
||||||
|
options = options.merge( rule.partial );
|
||||||
if( options == null )
|
|
||||||
{
|
|
||||||
options = rule.partial;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
|
|
||||||
if( !hasMany )
|
|
||||||
{
|
|
||||||
options = options.copy();
|
|
||||||
hasMany = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
options.merge( rule.partial );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (options == null ? PartialOptions.DEFAULT : options).toOptions();
|
return options.toOptions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@ import dan200.computercraft.ComputerCraft;
|
|||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.OptionalInt;
|
||||||
|
import java.util.OptionalLong;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,7 +50,7 @@ public class AddressRuleConfig
|
|||||||
public static boolean checkRule( UnmodifiableConfig builder )
|
public static boolean checkRule( UnmodifiableConfig builder )
|
||||||
{
|
{
|
||||||
String hostObj = get( builder, "host", String.class ).orElse( null );
|
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 )
|
return hostObj != null && checkEnum( builder, "action", Action.class )
|
||||||
&& check( builder, "port", Number.class )
|
&& check( builder, "port", Number.class )
|
||||||
&& check( builder, "timeout", Number.class )
|
&& check( builder, "timeout", Number.class )
|
||||||
@ -65,11 +67,11 @@ public class AddressRuleConfig
|
|||||||
if( hostObj == null ) return null;
|
if( hostObj == null ) return null;
|
||||||
|
|
||||||
Action action = getEnum( builder, "action", Action.class ).orElse( null );
|
Action action = getEnum( builder, "action", Action.class ).orElse( null );
|
||||||
Integer port = get( builder, "port", Number.class ).map( Number::intValue ).orElse( null );
|
OptionalInt port = unboxOptInt( get( builder, "port", Number.class ) );
|
||||||
Integer timeout = get( builder, "timeout", Number.class ).map( Number::intValue ).orElse( null );
|
OptionalInt timeout = unboxOptInt( get( builder, "timeout", Number.class ) );
|
||||||
Long maxUpload = get( builder, "max_upload", Number.class ).map( Number::longValue ).orElse( null );
|
OptionalLong maxUpload = unboxOptLong( get( builder, "max_upload", Number.class ).map( Number::longValue ) );
|
||||||
Long maxDownload = get( builder, "max_download", Number.class ).map( Number::longValue ).orElse( null );
|
OptionalLong maxDownload = unboxOptLong( get( builder, "max_download", Number.class ).map( Number::longValue ) );
|
||||||
Integer websocketMessage = get( builder, "websocket_message", Number.class ).map( Number::intValue ).orElse( null );
|
OptionalInt websocketMessage = unboxOptInt( get( builder, "websocket_message", Number.class ).map( Number::intValue ) );
|
||||||
|
|
||||||
PartialOptions options = new PartialOptions(
|
PartialOptions options = new PartialOptions(
|
||||||
action,
|
action,
|
||||||
@ -122,6 +124,16 @@ public class AddressRuleConfig
|
|||||||
return get( config, field, String.class ).map( x -> parseEnum( klass, x ) );
|
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
|
@Nullable
|
||||||
private static <T extends Enum<T>> T parseEnum( Class<T> klass, String x )
|
private static <T extends Enum<T>> T parseEnum( Class<T> klass, String x )
|
||||||
{
|
{
|
||||||
|
@ -5,21 +5,25 @@
|
|||||||
*/
|
*/
|
||||||
package dan200.computercraft.core.apis.http.options;
|
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
|
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;
|
private final @Nullable Action action;
|
||||||
Long maxUpload;
|
private final OptionalLong maxUpload;
|
||||||
Long maxDownload;
|
private final OptionalLong maxDownload;
|
||||||
Integer timeout;
|
private final OptionalInt timeout;
|
||||||
Integer websocketMessage;
|
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.action = action;
|
||||||
this.maxUpload = maxUpload;
|
this.maxUpload = maxUpload;
|
||||||
@ -28,31 +32,36 @@ public final class PartialOptions
|
|||||||
this.websocketMessage = websocketMessage;
|
this.websocketMessage = websocketMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
Options toOptions()
|
Options toOptions()
|
||||||
{
|
{
|
||||||
if( options != null ) return options;
|
if( options != null ) return options;
|
||||||
|
|
||||||
return options = new Options(
|
return options = new Options(
|
||||||
action == null ? Action.DENY : action,
|
action == null ? Action.DENY : action,
|
||||||
maxUpload == null ? AddressRule.MAX_UPLOAD : maxUpload,
|
maxUpload.orElse( AddressRule.MAX_UPLOAD ),
|
||||||
maxDownload == null ? AddressRule.MAX_DOWNLOAD : maxDownload,
|
maxDownload.orElse( AddressRule.MAX_DOWNLOAD ),
|
||||||
timeout == null ? AddressRule.TIMEOUT : timeout,
|
timeout.orElse( AddressRule.TIMEOUT ),
|
||||||
websocketMessage == null ? AddressRule.WEBSOCKET_MESSAGE : websocketMessage
|
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;
|
// Short circuit for DEFAULT. This has no effect on the outcome, but avoids an allocation.
|
||||||
if( maxUpload == null && other.maxUpload != null ) maxUpload = other.maxUpload;
|
if( this == DEFAULT ) return other;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
PartialOptions copy()
|
return new PartialOptions(
|
||||||
{
|
action == null && other.action != null ? other.action : action,
|
||||||
return new PartialOptions( action, maxUpload, maxDownload, timeout, websocketMessage );
|
maxUpload.isPresent() ? maxUpload : other.maxUpload,
|
||||||
|
maxDownload.isPresent() ? maxDownload : other.maxDownload,
|
||||||
|
timeout.isPresent() ? timeout : other.timeout,
|
||||||
|
websocketMessage.isPresent() ? websocketMessage : other.websocketMessage
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -26,7 +26,6 @@ import io.netty.handler.codec.http.HttpHeaderNames;
|
|||||||
import io.netty.handler.codec.http.HttpHeaders;
|
import io.netty.handler.codec.http.HttpHeaders;
|
||||||
import io.netty.handler.codec.http.HttpObjectAggregator;
|
import io.netty.handler.codec.http.HttpObjectAggregator;
|
||||||
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
|
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.codec.http.websocketx.WebSocketVersion;
|
||||||
import io.netty.handler.ssl.SslContext;
|
import io.netty.handler.ssl.SslContext;
|
||||||
|
|
||||||
@ -152,7 +151,7 @@ public class Websocket extends Resource<Websocket>
|
|||||||
}
|
}
|
||||||
|
|
||||||
String subprotocol = headers.get( HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL );
|
String subprotocol = headers.get( HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL );
|
||||||
WebSocketClientHandshaker handshaker = WebSocketClientHandshakerFactory.newHandshaker(
|
WebSocketClientHandshaker handshaker = new NoOriginWebSocketHanshakder(
|
||||||
uri, WebSocketVersion.V13, subprotocol, true, headers,
|
uri, WebSocketVersion.V13, subprotocol, true, headers,
|
||||||
options.websocketMessage <= 0 ? MAX_MESSAGE_SIZE : options.websocketMessage
|
options.websocketMessage <= 0 ? MAX_MESSAGE_SIZE : options.websocketMessage
|
||||||
);
|
);
|
||||||
|
@ -5,14 +5,11 @@
|
|||||||
*/
|
*/
|
||||||
package dan200.computercraft.core.computer;
|
package dan200.computercraft.core.computer;
|
||||||
|
|
||||||
import net.minecraft.core.Direction;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A side on a computer. Unlike {@link Direction}, this is relative to the direction the computer is
|
* A side on a computer. This is relative to the direction the computer is facing.
|
||||||
* facing..
|
|
||||||
*/
|
*/
|
||||||
public enum ComputerSide
|
public enum ComputerSide
|
||||||
{
|
{
|
||||||
|
@ -10,7 +10,6 @@ import dan200.computercraft.api.peripheral.IWorkMonitor;
|
|||||||
import dan200.computercraft.core.computer.Computer;
|
import dan200.computercraft.core.computer.Computer;
|
||||||
import dan200.computercraft.core.metrics.Metrics;
|
import dan200.computercraft.core.metrics.Metrics;
|
||||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
|
||||||
|
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
@ -28,7 +27,7 @@ import java.util.concurrent.TimeUnit;
|
|||||||
* this tick. At the beginning of the tick, we execute as many {@link MainThread} tasks as possible, until our
|
* 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.
|
* time-frame or the global time frame has expired.
|
||||||
* <p>
|
* <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)}.
|
* {@link IWorkMonitor#trackWork(long, TimeUnit)}.
|
||||||
* <p>
|
* <p>
|
||||||
* Now, if anywhere during this period, we use more than our allocated time slice, the executor is marked as
|
* Now, if anywhere during this period, we use more than our allocated time slice, the executor is marked as
|
||||||
|
@ -7,8 +7,6 @@ package dan200.computercraft.core.terminal;
|
|||||||
|
|
||||||
import dan200.computercraft.shared.util.Colour;
|
import dan200.computercraft.shared.util.Colour;
|
||||||
import dan200.computercraft.shared.util.Palette;
|
import dan200.computercraft.shared.util.Palette;
|
||||||
import net.minecraft.nbt.CompoundTag;
|
|
||||||
import net.minecraft.network.FriendlyByteBuf;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
@ -16,23 +14,23 @@ import java.nio.ByteBuffer;
|
|||||||
|
|
||||||
public class Terminal
|
public class Terminal
|
||||||
{
|
{
|
||||||
private static final String BASE_16 = "0123456789abcdef";
|
protected static final String BASE_16 = "0123456789abcdef";
|
||||||
|
|
||||||
private int width;
|
protected int width;
|
||||||
private int height;
|
protected int height;
|
||||||
private final boolean colour;
|
protected final boolean colour;
|
||||||
|
|
||||||
private int cursorX = 0;
|
protected int cursorX = 0;
|
||||||
private int cursorY = 0;
|
protected int cursorY = 0;
|
||||||
private boolean cursorBlink = false;
|
protected boolean cursorBlink = false;
|
||||||
private int cursorColour = 0;
|
protected int cursorColour = 0;
|
||||||
private int cursorBackgroundColour = 15;
|
protected int cursorBackgroundColour = 15;
|
||||||
|
|
||||||
private TextBuffer[] text;
|
protected TextBuffer[] text;
|
||||||
private TextBuffer[] textColour;
|
protected TextBuffer[] textColour;
|
||||||
private TextBuffer[] backgroundColour;
|
protected TextBuffer[] backgroundColour;
|
||||||
|
|
||||||
private final Palette palette;
|
protected final Palette palette;
|
||||||
|
|
||||||
private final @Nullable Runnable onChanged;
|
private final @Nullable Runnable onChanged;
|
||||||
|
|
||||||
@ -320,110 +318,6 @@ public class Terminal
|
|||||||
if( onChanged != null ) onChanged.run();
|
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 )
|
public static int getColour( char c, Colour def )
|
||||||
{
|
{
|
||||||
if( c >= '0' && c <= '9' ) return c - '0';
|
if( c >= '0' && c <= '9' ) return c - '0';
|
||||||
|
@ -7,7 +7,7 @@ package dan200.computercraft.shared;
|
|||||||
|
|
||||||
import dan200.computercraft.ComputerCraft;
|
import dan200.computercraft.ComputerCraft;
|
||||||
import dan200.computercraft.core.apis.http.NetworkUtils;
|
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.command.CommandComputerCraft;
|
||||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||||
import dan200.computercraft.shared.computer.metrics.ComputerMBean;
|
import dan200.computercraft.shared.computer.metrics.ComputerMBean;
|
||||||
|
@ -365,8 +365,7 @@ public final class CommandComputerCraft
|
|||||||
private static final List<AggregatedMetric> DEFAULT_FIELDS = Arrays.asList(
|
private static final List<AggregatedMetric> DEFAULT_FIELDS = Arrays.asList(
|
||||||
new AggregatedMetric( Metrics.COMPUTER_TASKS, Aggregate.COUNT ),
|
new AggregatedMetric( Metrics.COMPUTER_TASKS, Aggregate.COUNT ),
|
||||||
new AggregatedMetric( Metrics.COMPUTER_TASKS, Aggregate.NONE ),
|
new AggregatedMetric( Metrics.COMPUTER_TASKS, Aggregate.NONE ),
|
||||||
new AggregatedMetric( Metrics.COMPUTER_TASKS, Aggregate.AVG ),
|
new AggregatedMetric( Metrics.COMPUTER_TASKS, Aggregate.AVG )
|
||||||
new AggregatedMetric( Metrics.COMPUTER_TASKS, Aggregate.MAX )
|
|
||||||
);
|
);
|
||||||
|
|
||||||
private static int displayTimings( CommandSourceStack source, AggregatedMetric sortField, List<AggregatedMetric> fields ) throws CommandSyntaxException
|
private static int displayTimings( CommandSourceStack source, AggregatedMetric sortField, List<AggregatedMetric> fields ) throws CommandSyntaxException
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* 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.Cache;
|
||||||
import com.google.common.cache.CacheBuilder;
|
import com.google.common.cache.CacheBuilder;
|
||||||
@ -11,6 +11,7 @@ import com.google.common.io.ByteStreams;
|
|||||||
import dan200.computercraft.ComputerCraft;
|
import dan200.computercraft.ComputerCraft;
|
||||||
import dan200.computercraft.api.filesystem.IMount;
|
import dan200.computercraft.api.filesystem.IMount;
|
||||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
||||||
|
import dan200.computercraft.core.filesystem.FileSystem;
|
||||||
import dan200.computercraft.shared.util.IoUtil;
|
import dan200.computercraft.shared.util.IoUtil;
|
||||||
import net.minecraft.ResourceLocationException;
|
import net.minecraft.ResourceLocationException;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
@ -15,12 +15,12 @@ import dan200.computercraft.core.computer.Computer;
|
|||||||
import dan200.computercraft.core.computer.ComputerEnvironment;
|
import dan200.computercraft.core.computer.ComputerEnvironment;
|
||||||
import dan200.computercraft.core.computer.ComputerSide;
|
import dan200.computercraft.core.computer.ComputerSide;
|
||||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||||
import dan200.computercraft.core.terminal.Terminal;
|
|
||||||
import dan200.computercraft.shared.computer.menu.ComputerMenu;
|
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.NetworkHandler;
|
||||||
import dan200.computercraft.shared.network.NetworkMessage;
|
import dan200.computercraft.shared.network.NetworkMessage;
|
||||||
import dan200.computercraft.shared.network.client.ComputerTerminalClientMessage;
|
import dan200.computercraft.shared.network.client.ComputerTerminalClientMessage;
|
||||||
import dan200.computercraft.shared.network.client.TerminalState;
|
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
@ -41,7 +41,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment
|
|||||||
private final MetricsObserver metrics;
|
private final MetricsObserver metrics;
|
||||||
private final Computer computer;
|
private final Computer computer;
|
||||||
|
|
||||||
private final Terminal terminal;
|
private final NetworkedTerminal terminal;
|
||||||
private final AtomicBoolean terminalChanged = new AtomicBoolean( false );
|
private final AtomicBoolean terminalChanged = new AtomicBoolean( false );
|
||||||
|
|
||||||
private boolean changedLastFrame;
|
private boolean changedLastFrame;
|
||||||
@ -54,7 +54,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment
|
|||||||
|
|
||||||
ServerContext context = ServerContext.get( level.getServer() );
|
ServerContext context = ServerContext.get( level.getServer() );
|
||||||
instanceID = context.registry().getUnusedInstanceID();
|
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 );
|
metrics = context.metrics().createMetricObserver( this );
|
||||||
|
|
||||||
computer = new Computer( context.computerContext(), this, terminal, computerID );
|
computer = new Computer( context.computerContext(), this, terminal, computerID );
|
||||||
|
@ -5,13 +5,17 @@
|
|||||||
*/
|
*/
|
||||||
package dan200.computercraft.shared.computer.core;
|
package dan200.computercraft.shared.computer.core;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import dan200.computercraft.ComputerCraft;
|
import dan200.computercraft.ComputerCraft;
|
||||||
import dan200.computercraft.ComputerCraftAPIImpl;
|
import dan200.computercraft.ComputerCraftAPIImpl;
|
||||||
import dan200.computercraft.api.ComputerCraftAPI;
|
import dan200.computercraft.api.ComputerCraftAPI;
|
||||||
import dan200.computercraft.api.filesystem.IMount;
|
import dan200.computercraft.api.filesystem.IMount;
|
||||||
import dan200.computercraft.core.ComputerContext;
|
import dan200.computercraft.core.ComputerContext;
|
||||||
|
import dan200.computercraft.core.computer.ComputerThread;
|
||||||
import dan200.computercraft.core.computer.GlobalEnvironment;
|
import dan200.computercraft.core.computer.GlobalEnvironment;
|
||||||
import dan200.computercraft.core.computer.mainthread.MainThread;
|
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.CommonHooks;
|
||||||
import dan200.computercraft.shared.computer.metrics.GlobalMetrics;
|
import dan200.computercraft.shared.computer.metrics.GlobalMetrics;
|
||||||
import dan200.computercraft.shared.util.IDAssigner;
|
import dan200.computercraft.shared.util.IDAssigner;
|
||||||
@ -41,6 +45,9 @@ public final class ServerContext
|
|||||||
{
|
{
|
||||||
private static final LevelResource FOLDER = new LevelResource( ComputerCraft.MOD_ID );
|
private static final LevelResource FOLDER = new LevelResource( ComputerCraft.MOD_ID );
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public static ILuaMachine.Factory luaMachine = CobaltLuaMachine::new;
|
||||||
|
|
||||||
private static @Nullable ServerContext instance;
|
private static @Nullable ServerContext instance;
|
||||||
|
|
||||||
private final MinecraftServer server;
|
private final MinecraftServer server;
|
||||||
@ -57,7 +64,11 @@ public final class ServerContext
|
|||||||
this.server = server;
|
this.server = server;
|
||||||
storageDir = server.getWorldPath( FOLDER );
|
storageDir = server.getWorldPath( FOLDER );
|
||||||
mainThread = new MainThread();
|
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" ) );
|
idAssigner = new IDAssigner( storageDir.resolve( "ids.json" ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,8 @@ import dan200.computercraft.shared.computer.core.ServerComputer;
|
|||||||
import dan200.computercraft.shared.computer.menu.ComputerMenu;
|
import dan200.computercraft.shared.computer.menu.ComputerMenu;
|
||||||
import dan200.computercraft.shared.computer.menu.ServerInputHandler;
|
import dan200.computercraft.shared.computer.menu.ServerInputHandler;
|
||||||
import dan200.computercraft.shared.computer.menu.ServerInputState;
|
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.network.container.ComputerContainerData;
|
||||||
import dan200.computercraft.shared.util.SingleIntArray;
|
import dan200.computercraft.shared.util.SingleIntArray;
|
||||||
import net.minecraft.world.entity.player.Player;
|
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 ServerComputer computer;
|
||||||
private final @Nullable ServerInputState<ContainerComputerBase> input;
|
private final @Nullable ServerInputState<ContainerComputerBase> input;
|
||||||
|
|
||||||
private final @Nullable Terminal terminal;
|
private final @Nullable NetworkedTerminal terminal;
|
||||||
|
|
||||||
private final ItemStack displayStack;
|
private final ItemStack displayStack;
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
package dan200.computercraft.shared.computer.menu;
|
package dan200.computercraft.shared.computer.menu;
|
||||||
|
|
||||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
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;
|
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -3,9 +3,8 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* 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 dan200.computercraft.shared.util.IoUtil;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.buffer.ByteBufInputStream;
|
import io.netty.buffer.ByteBufInputStream;
|
||||||
@ -42,12 +41,12 @@ public class TerminalState
|
|||||||
|
|
||||||
private ByteBuf compressed;
|
private ByteBuf compressed;
|
||||||
|
|
||||||
public TerminalState( @Nullable Terminal terminal )
|
public TerminalState( @Nullable NetworkedTerminal terminal )
|
||||||
{
|
{
|
||||||
this( terminal, true );
|
this( terminal, true );
|
||||||
}
|
}
|
||||||
|
|
||||||
public TerminalState( @Nullable Terminal terminal, boolean compress )
|
public TerminalState( @Nullable NetworkedTerminal terminal, boolean compress )
|
||||||
{
|
{
|
||||||
this.compress = compress;
|
this.compress = compress;
|
||||||
|
|
||||||
@ -115,17 +114,17 @@ public class TerminalState
|
|||||||
return buffer == null ? 0 : buffer.readableBytes();
|
return buffer == null ? 0 : buffer.readableBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void apply( Terminal terminal )
|
public void apply( NetworkedTerminal terminal )
|
||||||
{
|
{
|
||||||
if( buffer == null ) throw new NullPointerException( "buffer" );
|
if( buffer == null ) throw new NullPointerException( "buffer" );
|
||||||
terminal.resize( width, height );
|
terminal.resize( width, height );
|
||||||
terminal.read( new FriendlyByteBuf( buffer ) );
|
terminal.read( new FriendlyByteBuf( buffer ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
public Terminal create()
|
public NetworkedTerminal create()
|
||||||
{
|
{
|
||||||
if( buffer == null ) throw new NullPointerException( "Terminal does not exist" );
|
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 ) );
|
terminal.read( new FriendlyByteBuf( buffer ) );
|
||||||
return terminal;
|
return terminal;
|
||||||
}
|
}
|
@ -13,8 +13,6 @@ public enum UploadResult
|
|||||||
CONSUMED,
|
CONSUMED,
|
||||||
ERROR;
|
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 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 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" );
|
public static final Component TOO_MUCH_MSG = Component.translatable( "gui.computercraft.upload.failed.too_much" );
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
package dan200.computercraft.shared.network.client;
|
package dan200.computercraft.shared.network.client;
|
||||||
|
|
||||||
import dan200.computercraft.shared.computer.menu.ComputerMenu;
|
import dan200.computercraft.shared.computer.menu.ComputerMenu;
|
||||||
|
import dan200.computercraft.shared.computer.terminal.TerminalState;
|
||||||
import dan200.computercraft.shared.network.NetworkMessage;
|
import dan200.computercraft.shared.network.NetworkMessage;
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
import net.minecraft.network.FriendlyByteBuf;
|
import net.minecraft.network.FriendlyByteBuf;
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
package dan200.computercraft.shared.network.client;
|
package dan200.computercraft.shared.network.client;
|
||||||
|
|
||||||
|
import dan200.computercraft.shared.computer.terminal.TerminalState;
|
||||||
import dan200.computercraft.shared.network.NetworkMessage;
|
import dan200.computercraft.shared.network.NetworkMessage;
|
||||||
import dan200.computercraft.shared.peripheral.monitor.TileMonitor;
|
import dan200.computercraft.shared.peripheral.monitor.TileMonitor;
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
|
@ -7,8 +7,9 @@ package dan200.computercraft.shared.network.client;
|
|||||||
|
|
||||||
import dan200.computercraft.client.pocket.ClientPocketComputers;
|
import dan200.computercraft.client.pocket.ClientPocketComputers;
|
||||||
import dan200.computercraft.client.pocket.PocketComputerData;
|
import dan200.computercraft.client.pocket.PocketComputerData;
|
||||||
import dan200.computercraft.core.terminal.Terminal;
|
|
||||||
import dan200.computercraft.shared.computer.core.ComputerState;
|
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.network.NetworkMessage;
|
||||||
import dan200.computercraft.shared.pocket.core.PocketServerComputer;
|
import dan200.computercraft.shared.pocket.core.PocketServerComputer;
|
||||||
import net.minecraft.network.FriendlyByteBuf;
|
import net.minecraft.network.FriendlyByteBuf;
|
||||||
@ -29,7 +30,7 @@ public class PocketComputerDataMessage implements NetworkMessage
|
|||||||
instanceId = computer.getInstanceID();
|
instanceId = computer.getInstanceID();
|
||||||
state = computer.getState();
|
state = computer.getState();
|
||||||
lightState = computer.getLight();
|
lightState = computer.getLight();
|
||||||
terminal = sendTerminal ? computer.getTerminalState() : new TerminalState( (Terminal) null );
|
terminal = sendTerminal ? computer.getTerminalState() : new TerminalState( (NetworkedTerminal) null );
|
||||||
}
|
}
|
||||||
|
|
||||||
public PocketComputerDataMessage( FriendlyByteBuf buf )
|
public PocketComputerDataMessage( FriendlyByteBuf buf )
|
||||||
|
@ -7,7 +7,7 @@ package dan200.computercraft.shared.network.container;
|
|||||||
|
|
||||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
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.network.FriendlyByteBuf;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ import javax.annotation.Nonnull;
|
|||||||
* :::
|
* :::
|
||||||
*
|
*
|
||||||
* @cc.module energy_storage
|
* @cc.module energy_storage
|
||||||
|
* @cc.since 1.94.0
|
||||||
*/
|
*/
|
||||||
public class EnergyMethods implements GenericPeripheral
|
public class EnergyMethods implements GenericPeripheral
|
||||||
{
|
{
|
||||||
|
@ -35,6 +35,7 @@ import static dan200.computercraft.shared.peripheral.generic.methods.ArgumentHel
|
|||||||
* Methods for interacting with tanks and other fluid storage blocks.
|
* Methods for interacting with tanks and other fluid storage blocks.
|
||||||
*
|
*
|
||||||
* @cc.module fluid_storage
|
* @cc.module fluid_storage
|
||||||
|
* @cc.since 1.94.0
|
||||||
*/
|
*/
|
||||||
public class FluidMethods implements GenericPeripheral
|
public class FluidMethods implements GenericPeripheral
|
||||||
{
|
{
|
||||||
|
@ -37,6 +37,7 @@ import static dan200.computercraft.shared.peripheral.generic.methods.ArgumentHel
|
|||||||
* Methods for interacting with inventories.
|
* Methods for interacting with inventories.
|
||||||
*
|
*
|
||||||
* @cc.module inventory
|
* @cc.module inventory
|
||||||
|
* @cc.since 1.94.0
|
||||||
*/
|
*/
|
||||||
public class InventoryMethods implements GenericPeripheral
|
public class InventoryMethods implements GenericPeripheral
|
||||||
{
|
{
|
||||||
@ -170,6 +171,7 @@ public class InventoryMethods implements GenericPeripheral
|
|||||||
* end
|
* end
|
||||||
* print(total)
|
* print(total)
|
||||||
* }</pre>
|
* }</pre>
|
||||||
|
* @cc.since 1.96.0
|
||||||
*/
|
*/
|
||||||
@LuaFunction( mainThread = true )
|
@LuaFunction( mainThread = true )
|
||||||
public static int getItemLimit( IItemHandler inventory, int slot ) throws LuaException
|
public static int getItemLimit( IItemHandler inventory, int slot ) throws LuaException
|
||||||
|
@ -9,7 +9,8 @@ import com.mojang.blaze3d.platform.GlStateManager;
|
|||||||
import dan200.computercraft.client.util.DirectBuffers;
|
import dan200.computercraft.client.util.DirectBuffers;
|
||||||
import dan200.computercraft.client.util.DirectVertexBuffer;
|
import dan200.computercraft.client.util.DirectVertexBuffer;
|
||||||
import dan200.computercraft.core.terminal.Terminal;
|
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.minecraft.core.BlockPos;
|
||||||
import net.minecraftforge.api.distmarker.Dist;
|
import net.minecraftforge.api.distmarker.Dist;
|
||||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||||
@ -36,7 +37,7 @@ public final class ClientMonitor
|
|||||||
public int tboUniform;
|
public int tboUniform;
|
||||||
public DirectVertexBuffer backgroundBuffer;
|
public DirectVertexBuffer backgroundBuffer;
|
||||||
public DirectVertexBuffer foregroundBuffer;
|
public DirectVertexBuffer foregroundBuffer;
|
||||||
private Terminal terminal;
|
private NetworkedTerminal terminal;
|
||||||
private boolean terminalChanged;
|
private boolean terminalChanged;
|
||||||
|
|
||||||
public ClientMonitor( TileMonitor origin )
|
public ClientMonitor( TileMonitor origin )
|
||||||
@ -182,7 +183,7 @@ public final class ClientMonitor
|
|||||||
{
|
{
|
||||||
if( state.hasTerminal() )
|
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 );
|
state.apply( terminal );
|
||||||
terminalChanged = true;
|
terminalChanged = true;
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ package dan200.computercraft.shared.peripheral.monitor;
|
|||||||
import dan200.computercraft.ComputerCraft;
|
import dan200.computercraft.ComputerCraft;
|
||||||
import dan200.computercraft.shared.network.NetworkHandler;
|
import dan200.computercraft.shared.network.NetworkHandler;
|
||||||
import dan200.computercraft.shared.network.client.MonitorClientMessage;
|
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.core.BlockPos;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
import net.minecraft.world.level.Level;
|
import net.minecraft.world.level.Level;
|
||||||
|
@ -7,6 +7,7 @@ package dan200.computercraft.shared.peripheral.monitor;
|
|||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import dan200.computercraft.core.terminal.Terminal;
|
import dan200.computercraft.core.terminal.Terminal;
|
||||||
|
import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
|
||||||
import dan200.computercraft.shared.util.TickScheduler;
|
import dan200.computercraft.shared.util.TickScheduler;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
@ -18,7 +19,7 @@ public class ServerMonitor
|
|||||||
|
|
||||||
private final boolean colour;
|
private final boolean colour;
|
||||||
private int textScale = 2;
|
private int textScale = 2;
|
||||||
private @Nullable Terminal terminal;
|
private @Nullable NetworkedTerminal terminal;
|
||||||
private final AtomicBoolean resized = new AtomicBoolean( false );
|
private final AtomicBoolean resized = new AtomicBoolean( false );
|
||||||
private final AtomicBoolean changed = new AtomicBoolean( false );
|
private final AtomicBoolean changed = new AtomicBoolean( false );
|
||||||
|
|
||||||
@ -46,7 +47,7 @@ public class ServerMonitor
|
|||||||
|
|
||||||
if( terminal == null )
|
if( terminal == null )
|
||||||
{
|
{
|
||||||
terminal = new Terminal( termWidth, termHeight, colour, this::markChanged );
|
terminal = new NetworkedTerminal( termWidth, termHeight, colour, this::markChanged );
|
||||||
markChanged();
|
markChanged();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -91,7 +92,7 @@ public class ServerMonitor
|
|||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public Terminal getTerminal()
|
public NetworkedTerminal getTerminal()
|
||||||
{
|
{
|
||||||
return terminal;
|
return terminal;
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import dan200.computercraft.api.peripheral.IComputerAccess;
|
|||||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||||
import dan200.computercraft.core.terminal.Terminal;
|
import dan200.computercraft.core.terminal.Terminal;
|
||||||
import dan200.computercraft.shared.common.TileGeneric;
|
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.CapabilityUtil;
|
||||||
import dan200.computercraft.shared.util.TickScheduler;
|
import dan200.computercraft.shared.util.TickScheduler;
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
|
@ -8,6 +8,7 @@ package dan200.computercraft.shared.peripheral.printer;
|
|||||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||||
import dan200.computercraft.core.terminal.Terminal;
|
import dan200.computercraft.core.terminal.Terminal;
|
||||||
import dan200.computercraft.shared.common.TileGeneric;
|
import dan200.computercraft.shared.common.TileGeneric;
|
||||||
|
import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
|
||||||
import dan200.computercraft.shared.media.items.ItemPrintout;
|
import dan200.computercraft.shared.media.items.ItemPrintout;
|
||||||
import dan200.computercraft.shared.util.*;
|
import dan200.computercraft.shared.util.*;
|
||||||
import net.minecraft.core.BlockPos;
|
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 ) );
|
SidedCaps.ofNullable( facing -> facing == null ? new InvWrapper( this ) : new SidedInvWrapper( this, facing ) );
|
||||||
private LazyOptional<IPeripheral> peripheralCap;
|
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 String pageTitle = "";
|
||||||
private boolean printing = false;
|
private boolean printing = false;
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ import java.io.IOException;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -177,7 +178,7 @@ public final class NBTUtil
|
|||||||
{
|
{
|
||||||
MessageDigest digest = MessageDigest.getInstance( "MD5" );
|
MessageDigest digest = MessageDigest.getInstance( "MD5" );
|
||||||
DataOutput output = new DataOutputStream( new DigestOutputStream( digest ) );
|
DataOutput output = new DataOutputStream( new DigestOutputStream( digest ) );
|
||||||
NbtIo.write( tag, output );
|
writeTag( output, "", tag );
|
||||||
byte[] hash = digest.digest();
|
byte[] hash = digest.digest();
|
||||||
return ENCODING.encode( hash );
|
return ENCODING.encode( hash );
|
||||||
}
|
}
|
||||||
@ -188,6 +189,37 @@ public final class NBTUtil
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An alternative version of {@link NbtIo#write(CompoundTag, DataOutput)}, which sorts keys. This
|
||||||
|
* should make the output slightly more deterministic.
|
||||||
|
*
|
||||||
|
* @param output The output to write to.
|
||||||
|
* @param name The name of the key we're writing. Should be {@code ""} for the root node.
|
||||||
|
* @param tag The tag to write.
|
||||||
|
* @throws IOException If the underlying stream throws.
|
||||||
|
* @see NbtIo#write(CompoundTag, DataOutput)
|
||||||
|
* @see CompoundTag#write(DataOutput)
|
||||||
|
*/
|
||||||
|
private static void writeTag( DataOutput output, String name, Tag tag ) throws IOException
|
||||||
|
{
|
||||||
|
output.writeByte( tag.getId() );
|
||||||
|
if( tag.getId() == 0 ) return;
|
||||||
|
output.writeUTF( name );
|
||||||
|
|
||||||
|
if( tag instanceof CompoundTag compound )
|
||||||
|
{
|
||||||
|
String[] keys = compound.getAllKeys().toArray( new String[0] );
|
||||||
|
Arrays.sort( keys );
|
||||||
|
for( String key : keys ) writeTag( output, key, compound.get( key ) );
|
||||||
|
|
||||||
|
output.writeByte( 0 );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tag.write( output );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class DigestOutputStream extends OutputStream
|
private static final class DigestOutputStream extends OutputStream
|
||||||
{
|
{
|
||||||
private final MessageDigest digest;
|
private final MessageDigest digest;
|
||||||
|
@ -111,13 +111,10 @@
|
|||||||
"gui.computercraft.tooltip.computer_id": "Computer ID: %s",
|
"gui.computercraft.tooltip.computer_id": "Computer ID: %s",
|
||||||
"gui.computercraft.tooltip.disk_id": "Disk ID: %s",
|
"gui.computercraft.tooltip.disk_id": "Disk ID: %s",
|
||||||
"gui.computercraft.tooltip.turn_on": "Turn this computer on",
|
"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": "Turn this computer off",
|
||||||
"gui.computercraft.tooltip.turn_off.key": "Hold Ctrl+S",
|
"gui.computercraft.tooltip.turn_off.key": "Hold Ctrl+S",
|
||||||
"gui.computercraft.tooltip.terminate": "Stop the currently running code",
|
"gui.computercraft.tooltip.terminate": "Stop the currently running code",
|
||||||
"gui.computercraft.tooltip.terminate.key": "Hold Ctrl+T",
|
"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": "Upload Failed",
|
||||||
"gui.computercraft.upload.failed.computer_off": "You must turn the computer on before uploading files.",
|
"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.",
|
"gui.computercraft.upload.failed.too_much": "Your files are too large to be uploaded.",
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
-- Ideally we'd use require, but that is part of the shell, and so is not
|
-- 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
|
-- available to the BIOS or any APIs. All APIs load this using dofile, but that
|
||||||
-- has not been defined at this point.
|
-- has not been defined at this point.
|
||||||
local expect
|
local expect, field
|
||||||
|
|
||||||
do
|
do
|
||||||
local h = fs.open("rom/modules/main/cc/expect.lua", "r")
|
local h = fs.open("rom/modules/main/cc/expect.lua", "r")
|
||||||
@ -11,7 +11,8 @@ do
|
|||||||
h.close()
|
h.close()
|
||||||
|
|
||||||
if not f then error(err) end
|
if not f then error(err) end
|
||||||
expect = f().expect
|
local res = f()
|
||||||
|
expect, field = res.expect, res.field
|
||||||
end
|
end
|
||||||
|
|
||||||
if _VERSION == "Lua 5.1" then
|
if _VERSION == "Lua 5.1" then
|
||||||
@ -716,9 +717,17 @@ local tEmpty = {}
|
|||||||
function fs.complete(sPath, sLocation, bIncludeFiles, bIncludeDirs)
|
function fs.complete(sPath, sLocation, bIncludeFiles, bIncludeDirs)
|
||||||
expect(1, sPath, "string")
|
expect(1, sPath, "string")
|
||||||
expect(2, sLocation, "string")
|
expect(2, sLocation, "string")
|
||||||
expect(3, bIncludeFiles, "boolean", "nil")
|
local bIncludeHidden = nil
|
||||||
expect(4, bIncludeDirs, "boolean", "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
|
bIncludeFiles = bIncludeFiles ~= false
|
||||||
bIncludeDirs = bIncludeDirs ~= false
|
bIncludeDirs = bIncludeDirs ~= false
|
||||||
local sDir = sLocation
|
local sDir = sLocation
|
||||||
@ -755,7 +764,9 @@ function fs.complete(sPath, sLocation, bIncludeFiles, bIncludeDirs)
|
|||||||
local tFiles = fs.list(sDir)
|
local tFiles = fs.list(sDir)
|
||||||
for n = 1, #tFiles do
|
for n = 1, #tFiles do
|
||||||
local sFile = tFiles[n]
|
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 bIsDir = fs.isDir(fs.combine(sDir, sFile))
|
||||||
local sResult = string.sub(sFile, #sName + 1)
|
local sResult = string.sub(sFile, #sName + 1)
|
||||||
if bIsDir then
|
if bIsDir then
|
||||||
@ -902,7 +913,7 @@ settings.define("paint.default_extension", {
|
|||||||
|
|
||||||
settings.define("list.show_hidden", {
|
settings.define("list.show_hidden", {
|
||||||
default = false,
|
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",
|
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.",
|
description = "Prevents assigning variables into a program's environment. Make sure you use the local keyword or assign to _G explicitly.",
|
||||||
type = "boolean",
|
type = "boolean",
|
||||||
})
|
})
|
||||||
|
settings.define("shell.autocomplete_hidden", {
|
||||||
|
default = false,
|
||||||
|
description = [[Autocomplete hidden files and folders (those starting with ".").]],
|
||||||
|
type = "boolean",
|
||||||
|
})
|
||||||
|
|
||||||
if term.isColour() then
|
if term.isColour() then
|
||||||
settings.define("bios.use_multishell", {
|
settings.define("bios.use_multishell", {
|
||||||
|
@ -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
|
# New features in CC: Tweaked 1.100.10
|
||||||
|
|
||||||
* Mention WAV support in speaker help (MCJack123).
|
* Mention WAV support in speaker help (MCJack123).
|
||||||
* Add http programs to the path, even when http is not enabled.
|
* Add http programs to the path, even when http is not enabled.
|
||||||
|
|
||||||
Several bug fixes:
|
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.
|
* Fix help program treating the terminal one line longer than it was.
|
||||||
* Send block updates to client when turtle moves (roland-a).
|
* Send block updates to client when turtle moves (roland-a).
|
||||||
* Resolve several monitor issues when running Occulus shaders.
|
* Resolve several monitor issues when running Occulus shaders.
|
||||||
@ -236,7 +254,7 @@ And several bug fixes:
|
|||||||
# New features in CC: Tweaked 1.96.0
|
# New features in CC: Tweaked 1.96.0
|
||||||
|
|
||||||
* Use lightGrey for folders within the "list" program.
|
* 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.
|
* Expose the generic peripheral system to the public API.
|
||||||
* Add cc.expect.range (Lupus590).
|
* Add cc.expect.range (Lupus590).
|
||||||
* Allow calling cc.expect directly (MCJack123).
|
* Allow calling cc.expect directly (MCJack123).
|
||||||
|
@ -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).
|
* Improvee Dutch translation (Quezler)
|
||||||
* Add http programs to the path, even when http is not enabled.
|
* 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:
|
Several bug fixes:
|
||||||
* Fix example in textutils.pagedTabulate docs (IvoLeal72).
|
* Prevent `edit`'s "Run" command scrolling the terminal output on smaller
|
||||||
* Fix help program treating the terminal one line longer than it was.
|
screens.
|
||||||
* Send block updates to client when turtle moves (roland-a).
|
* Remove some non-determinism in computing item's `nbt` hash.
|
||||||
* Resolve several monitor issues when running Occulus shaders.
|
* Don't set the `Origin` header on outgoing websocket requests.
|
||||||
|
|
||||||
Type "help changelog" to see the full version history.
|
Type "help changelog" to see the full version history.
|
||||||
|
@ -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 guide!speaker_audio Gives a more general introduction to audio processing and the speaker.
|
||||||
@see speaker.playAudio To play the decoded audio data.
|
@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
|
@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.
|
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
|
```lua
|
||||||
local dfpwm = require("cc.audio.dfpwm")
|
local dfpwm = require("cc.audio.dfpwm")
|
||||||
|
@ -34,7 +34,11 @@ local completion = require "cc.completion"
|
|||||||
-- @tparam string text Current text to complete.
|
-- @tparam string text Current text to complete.
|
||||||
-- @treturn { string... } A list of suffixes of matching files.
|
-- @treturn { string... } A list of suffixes of matching files.
|
||||||
local function file(shell, text)
|
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
|
end
|
||||||
|
|
||||||
--- Complete the name of a directory relative to the current working directory.
|
--- Complete the name of a directory relative to the current working directory.
|
||||||
@ -43,7 +47,11 @@ end
|
|||||||
-- @tparam string text Current text to complete.
|
-- @tparam string text Current text to complete.
|
||||||
-- @treturn { string... } A list of suffixes of matching directories.
|
-- @treturn { string... } A list of suffixes of matching directories.
|
||||||
local function dir(shell, text)
|
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
|
end
|
||||||
|
|
||||||
--- Complete the name of a file or directory relative to the current working
|
--- 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.
|
-- @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.
|
-- @treturn { string... } A list of suffixes of matching files and directories.
|
||||||
local function dirOrFile(shell, text, previous, add_space)
|
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
|
if add_space then
|
||||||
for n = 1, #results do
|
for n = 1, #results do
|
||||||
local result = results[n]
|
local result = results[n]
|
||||||
|
@ -55,19 +55,22 @@ term.redirect(current)
|
|||||||
term.setTextColor(term.isColour() and colours.yellow or colours.white)
|
term.setTextColor(term.isColour() and colours.yellow or colours.white)
|
||||||
term.setBackgroundColor(colours.black)
|
term.setBackgroundColor(colours.black)
|
||||||
term.setCursorBlink(false)
|
term.setCursorBlink(false)
|
||||||
local _, y = term.getCursorPos()
|
|
||||||
local _, h = term.getSize()
|
|
||||||
if not ok then
|
if not ok then
|
||||||
printError(err)
|
printError(err)
|
||||||
end
|
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
|
end
|
||||||
term.setCursorPos(1, h)
|
|
||||||
if ok then
|
|
||||||
write("Program finished. ")
|
|
||||||
end
|
|
||||||
write("Press any key to continue")
|
|
||||||
os.pullEvent('key')
|
os.pullEvent('key')
|
||||||
]]
|
]]
|
||||||
|
|
||||||
|
@ -329,9 +329,14 @@ function shell.programs(include_hidden)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function completeProgram(sLine)
|
local function completeProgram(sLine)
|
||||||
|
local bIncludeHidden = settings.get("shell.autocomplete_hidden")
|
||||||
if #sLine > 0 and (sLine:find("/") or sLine:find("\\")) then
|
if #sLine > 0 and (sLine:find("/") or sLine:find("\\")) then
|
||||||
-- Add programs from the root
|
-- 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
|
else
|
||||||
local tResults = {}
|
local tResults = {}
|
||||||
@ -349,7 +354,11 @@ local function completeProgram(sLine)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Add all subdirectories. We don't include files as they will be added in the block below
|
-- 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
|
for i = 1, #tDirs do
|
||||||
local sResult = tDirs[i]
|
local sResult = tDirs[i]
|
||||||
if not tSeen[sResult] then
|
if not tSeen[sResult] then
|
||||||
|
@ -12,6 +12,7 @@ import org.junit.jupiter.params.provider.ValueSource;
|
|||||||
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.OptionalInt;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
@ -21,8 +22,8 @@ public class AddressRuleTest
|
|||||||
public void matchesPort()
|
public void matchesPort()
|
||||||
{
|
{
|
||||||
Iterable<AddressRule> rules = Collections.singletonList( AddressRule.parse(
|
Iterable<AddressRule> rules = Collections.singletonList( AddressRule.parse(
|
||||||
"127.0.0.1", 8080,
|
"127.0.0.1", OptionalInt.of( 8080 ),
|
||||||
new PartialOptions( Action.ALLOW, null, null, null, null )
|
Action.ALLOW.toPartial()
|
||||||
) );
|
) );
|
||||||
|
|
||||||
assertEquals( apply( rules, "localhost", 8080 ).action, Action.ALLOW );
|
assertEquals( apply( rules, "localhost", 8080 ).action, Action.ALLOW );
|
||||||
|
@ -9,9 +9,6 @@ import dan200.computercraft.api.lua.LuaValues;
|
|||||||
import dan200.computercraft.shared.util.Colour;
|
import dan200.computercraft.shared.util.Colour;
|
||||||
import dan200.computercraft.test.core.CallCounter;
|
import dan200.computercraft.test.core.CallCounter;
|
||||||
import dan200.computercraft.test.core.terminal.TerminalMatchers;
|
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.hamcrest.Matcher;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
@ -597,92 +594,6 @@ class TerminalTest
|
|||||||
callCounter.assertNotCalled();
|
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
|
@Test
|
||||||
void testGetColour()
|
void testGetColour()
|
||||||
{
|
{
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
*/
|
*/
|
||||||
package dan200.computercraft.core.filesystem;
|
package dan200.computercraft.shared.computer.core;
|
||||||
|
|
||||||
import dan200.computercraft.api.filesystem.IMount;
|
import dan200.computercraft.api.filesystem.IMount;
|
||||||
import net.minecraft.Util;
|
import net.minecraft.Util;
|
@ -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 ) );
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* 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.Terminal;
|
||||||
import dan200.computercraft.core.terminal.TextBuffer;
|
import dan200.computercraft.core.terminal.TextBuffer;
|
||||||
@ -23,7 +23,7 @@ public class TerminalStateTest
|
|||||||
@RepeatedTest( 5 )
|
@RepeatedTest( 5 )
|
||||||
public void testCompressed()
|
public void testCompressed()
|
||||||
{
|
{
|
||||||
Terminal terminal = randomTerminal();
|
var terminal = randomTerminal();
|
||||||
|
|
||||||
FriendlyByteBuf buffer = new FriendlyByteBuf( Unpooled.directBuffer() );
|
FriendlyByteBuf buffer = new FriendlyByteBuf( Unpooled.directBuffer() );
|
||||||
new TerminalState( terminal, true ).write( buffer );
|
new TerminalState( terminal, true ).write( buffer );
|
||||||
@ -35,7 +35,7 @@ public class TerminalStateTest
|
|||||||
@RepeatedTest( 5 )
|
@RepeatedTest( 5 )
|
||||||
public void testUncompressed()
|
public void testUncompressed()
|
||||||
{
|
{
|
||||||
Terminal terminal = randomTerminal();
|
var terminal = randomTerminal();
|
||||||
|
|
||||||
FriendlyByteBuf buffer = new FriendlyByteBuf( Unpooled.directBuffer() );
|
FriendlyByteBuf buffer = new FriendlyByteBuf( Unpooled.directBuffer() );
|
||||||
new TerminalState( terminal, false ).write( buffer );
|
new TerminalState( terminal, false ).write( buffer );
|
||||||
@ -44,10 +44,10 @@ public class TerminalStateTest
|
|||||||
assertEquals( 0, buffer.readableBytes() );
|
assertEquals( 0, buffer.readableBytes() );
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Terminal randomTerminal()
|
private static NetworkedTerminal randomTerminal()
|
||||||
{
|
{
|
||||||
Random random = new Random();
|
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++ )
|
for( int y = 0; y < terminal.getHeight(); y++ )
|
||||||
{
|
{
|
||||||
TextBuffer buffer = terminal.getLine( y );
|
TextBuffer buffer = terminal.getLine( y );
|
||||||
@ -70,14 +70,14 @@ public class TerminalStateTest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Terminal read( FriendlyByteBuf buffer )
|
private static NetworkedTerminal read( FriendlyByteBuf buffer )
|
||||||
{
|
{
|
||||||
TerminalState state = new TerminalState( buffer );
|
TerminalState state = new TerminalState( buffer );
|
||||||
assertTrue( state.colour );
|
assertTrue( state.colour );
|
||||||
|
|
||||||
if( !state.hasTerminal() ) return null;
|
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 );
|
state.apply( other );
|
||||||
return other;
|
return other;
|
||||||
}
|
}
|
@ -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, "", "", 1):eq("bad argument #3 (expected boolean, got number)")
|
||||||
expect.error(fs.complete, "", "", true, 1):eq("bad argument #4 (expected boolean, got number)")
|
expect.error(fs.complete, "", "", true, 1):eq("bad argument #4 (expected boolean, got number)")
|
||||||
end)
|
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)
|
end)
|
||||||
|
|
||||||
describe("fs.isDriveRoot", function()
|
describe("fs.isDriveRoot", function()
|
||||||
|
@ -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.
|
-- 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", "Sun")
|
||||||
-- exp_code("%A", "Sunday")
|
exp_code("%A", "Sunday")
|
||||||
exp_code("%b", "Oct")
|
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", "Sun Oct 1 23:12:17 2000")
|
||||||
exp_code("%C", "20")
|
exp_code("%C", "20")
|
||||||
exp_code("%d", "01")
|
exp_code("%d", "01")
|
||||||
|
@ -5,11 +5,9 @@ import dan200.computercraft.api.lua.ILuaContext
|
|||||||
import dan200.computercraft.core.lua.ILuaMachine
|
import dan200.computercraft.core.lua.ILuaMachine
|
||||||
import dan200.computercraft.core.lua.MachineEnvironment
|
import dan200.computercraft.core.lua.MachineEnvironment
|
||||||
import dan200.computercraft.core.lua.MachineResult
|
import dan200.computercraft.core.lua.MachineResult
|
||||||
import kotlinx.coroutines.CoroutineName
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An [ILuaMachine] which runs Kotlin functions instead.
|
* An [ILuaMachine] which runs Kotlin functions instead.
|
||||||
@ -26,7 +24,7 @@ abstract class KotlinLuaMachine(environment: MachineEnvironment) : ILuaMachine,
|
|||||||
queueEvent(eventName, arguments)
|
queueEvent(eventName, arguments)
|
||||||
} else {
|
} else {
|
||||||
val task = getTask()
|
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
|
return MachineResult.OK
|
||||||
@ -38,4 +36,20 @@ abstract class KotlinLuaMachine(environment: MachineEnvironment) : ILuaMachine,
|
|||||||
* Get the next task to execute on this computer.
|
* Get the next task to execute on this computer.
|
||||||
*/
|
*/
|
||||||
protected abstract fun getTask(): (suspend KotlinLuaMachine.() -> Unit)?
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,11 @@ import dan200.computercraft.api.lua.ILuaAPI
|
|||||||
import dan200.computercraft.api.lua.ILuaContext
|
import dan200.computercraft.api.lua.ILuaContext
|
||||||
import dan200.computercraft.api.lua.MethodResult
|
import dan200.computercraft.api.lua.MethodResult
|
||||||
import dan200.computercraft.api.lua.ObjectArguments
|
import dan200.computercraft.api.lua.ObjectArguments
|
||||||
|
import dan200.computercraft.core.apis.OSAPI
|
||||||
import dan200.computercraft.core.apis.PeripheralAPI
|
import dan200.computercraft.core.apis.PeripheralAPI
|
||||||
import kotlinx.coroutines.CancellableContinuation
|
import kotlinx.coroutines.CancellableContinuation
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The context for tasks which consume Lua objects.
|
* The context for tasks which consume Lua objects.
|
||||||
@ -40,6 +42,19 @@ interface LuaTaskContext {
|
|||||||
/** Call a peripheral method. */
|
/** Call a peripheral method. */
|
||||||
suspend fun LuaTaskContext.callPeripheral(name: String, method: String, vararg args: Any?): Array<out Any?>? =
|
suspend fun LuaTaskContext.callPeripheral(name: String, method: String, vararg args: Any?): Array<out Any?>? =
|
||||||
getApi<PeripheralAPI>().call(context, ObjectArguments(name, method, *args)).await()
|
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. */
|
/** Get a registered API. */
|
||||||
|
@ -10,8 +10,11 @@ import com.google.common.io.RecursiveDeleteOption;
|
|||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
import com.mojang.blaze3d.systems.RenderSystem;
|
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.ComputerCraft;
|
||||||
import dan200.computercraft.ingame.mod.TestMod;
|
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
import net.minecraft.core.NonNullList;
|
import net.minecraft.core.NonNullList;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
@ -19,10 +22,6 @@ import net.minecraft.resources.ResourceLocation;
|
|||||||
import net.minecraft.world.item.Item;
|
import net.minecraft.world.item.Item;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.minecraft.world.item.crafting.*;
|
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 net.minecraftforge.registries.ForgeRegistries;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -37,19 +36,24 @@ import java.util.Set;
|
|||||||
/**
|
/**
|
||||||
* Provides a {@literal /ccexport <path>} command which exports icons and recipes for all ComputerCraft items.
|
* 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
|
public class Exporter
|
||||||
{
|
{
|
||||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||||
|
|
||||||
@SubscribeEvent
|
public static <S> void register( CommandDispatcher<S> dispatcher )
|
||||||
public static void onClientCommands( ClientChatEvent event )
|
|
||||||
{
|
{
|
||||||
String prefix = "/ccexport";
|
dispatcher.register(
|
||||||
if( !event.getMessage().startsWith( prefix ) ) return;
|
LiteralArgumentBuilder.<S>literal( "ccexport" )
|
||||||
event.setCanceled( true );
|
.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 ) )
|
if( !Files.isDirectory( output ) )
|
||||||
{
|
{
|
||||||
Minecraft.getInstance().gui.getChat().addMessage( Component.literal( "Output path does not exist" ) );
|
Minecraft.getInstance().gui.getChat().addMessage( Component.literal( "Output path does not exist" ) );
|
||||||
|
@ -3,9 +3,9 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* 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.GameTestAssertException;
|
||||||
import net.minecraft.gametest.framework.GameTestSequence;
|
import net.minecraft.gametest.framework.GameTestSequence;
|
||||||
|
|
@ -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
|
||||||
|
{
|
||||||
|
}
|
@ -3,17 +3,16 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
*/
|
*/
|
||||||
package dan200.computercraft.ingame.mod;
|
package dan200.computercraft.gametest.core;
|
||||||
|
|
||||||
import com.mojang.brigadier.CommandDispatcher;
|
import com.mojang.brigadier.CommandDispatcher;
|
||||||
import dan200.computercraft.ComputerCraft;
|
import dan200.computercraft.ComputerCraft;
|
||||||
|
import dan200.computercraft.mixin.gametest.TestCommandAccessor;
|
||||||
import net.minecraft.ChatFormatting;
|
import net.minecraft.ChatFormatting;
|
||||||
import net.minecraft.client.Minecraft;
|
|
||||||
import net.minecraft.commands.CommandSourceStack;
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.gametest.framework.GameTestRegistry;
|
import net.minecraft.gametest.framework.GameTestRegistry;
|
||||||
import net.minecraft.gametest.framework.StructureUtils;
|
import net.minecraft.gametest.framework.StructureUtils;
|
||||||
import net.minecraft.gametest.framework.TestCommand;
|
|
||||||
import net.minecraft.gametest.framework.TestFunction;
|
import net.minecraft.gametest.framework.TestFunction;
|
||||||
import net.minecraft.nbt.CompoundTag;
|
import net.minecraft.nbt.CompoundTag;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
@ -24,10 +23,10 @@ import net.minecraft.world.entity.EntityType;
|
|||||||
import net.minecraft.world.entity.decoration.ArmorStand;
|
import net.minecraft.world.entity.decoration.ArmorStand;
|
||||||
import net.minecraft.world.level.block.entity.StructureBlockEntity;
|
import net.minecraft.world.level.block.entity.StructureBlockEntity;
|
||||||
import net.minecraft.world.level.storage.LevelResource;
|
import net.minecraft.world.level.storage.LevelResource;
|
||||||
import net.minecraftforge.fml.loading.FMLLoader;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
import static dan200.computercraft.shared.command.builder.HelpingArgumentBuilder.choice;
|
import static dan200.computercraft.shared.command.builder.HelpingArgumentBuilder.choice;
|
||||||
import static net.minecraft.commands.Commands.literal;
|
import static net.minecraft.commands.Commands.literal;
|
||||||
@ -37,6 +36,8 @@ import static net.minecraft.commands.Commands.literal;
|
|||||||
*/
|
*/
|
||||||
class CCTestCommand
|
class CCTestCommand
|
||||||
{
|
{
|
||||||
|
public static final LevelResource LOCATION = new LevelResource( ComputerCraft.MOD_ID );
|
||||||
|
|
||||||
public static void register( CommandDispatcher<CommandSourceStack> dispatcher )
|
public static void register( CommandDispatcher<CommandSourceStack> dispatcher )
|
||||||
{
|
{
|
||||||
dispatcher.register( choice( "cctest" )
|
dispatcher.register( choice( "cctest" )
|
||||||
@ -49,7 +50,7 @@ class CCTestCommand
|
|||||||
|
|
||||||
for( TestFunction function : GameTestRegistry.getAllTestFunctions() )
|
for( TestFunction function : GameTestRegistry.getAllTestFunctions() )
|
||||||
{
|
{
|
||||||
TestCommand.exportTestStructure( context.getSource(), function.getStructureName() );
|
TestCommandAccessor.callExportTestStructure( context.getSource(), function.getStructureName() );
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
} ) )
|
} ) )
|
||||||
@ -57,17 +58,11 @@ class CCTestCommand
|
|||||||
for( TestFunction function : GameTestRegistry.getAllTestFunctions() )
|
for( TestFunction function : GameTestRegistry.getAllTestFunctions() )
|
||||||
{
|
{
|
||||||
dispatcher.execute( "test import " + function.getTestName(), context.getSource() );
|
dispatcher.execute( "test import " + function.getTestName(), context.getSource() );
|
||||||
TestCommand.exportTestStructure( context.getSource(), function.getStructureName() );
|
TestCommandAccessor.callExportTestStructure( context.getSource(), function.getStructureName() );
|
||||||
}
|
}
|
||||||
return 0;
|
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 -> {
|
.then( literal( "marker" ).executes( context -> {
|
||||||
ServerPlayer player = context.getSource().getPlayerOrException();
|
ServerPlayer player = context.getSource().getPlayerOrException();
|
||||||
BlockPos pos = StructureUtils.findNearestStructureBlock( player.blockPosition(), 15, player.getLevel() );
|
BlockPos pos = StructureUtils.findNearestStructureBlock( player.blockPosition(), 15, player.getLevel() );
|
||||||
@ -99,7 +94,7 @@ class CCTestCommand
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Copier.replicate( TestMod.sourceDir.resolve( "computers" ), server.getWorldPath( new LevelResource( ComputerCraft.MOD_ID ) ) );
|
Copier.replicate( getSourceComputerPath(), getWorldComputerPath( server ) );
|
||||||
}
|
}
|
||||||
catch( IOException e )
|
catch( IOException e )
|
||||||
{
|
{
|
||||||
@ -111,7 +106,7 @@ class CCTestCommand
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Copier.replicate( server.getWorldPath( new LevelResource( ComputerCraft.MOD_ID ) ), TestMod.sourceDir.resolve( "computers" ) );
|
Copier.replicate( getWorldComputerPath( server ), getSourceComputerPath() );
|
||||||
}
|
}
|
||||||
catch( IOException e )
|
catch( IOException e )
|
||||||
{
|
{
|
||||||
@ -119,20 +114,14 @@ class CCTestCommand
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void promote()
|
private static Path getWorldComputerPath( MinecraftServer server )
|
||||||
{
|
{
|
||||||
try
|
return server.getWorldPath( LOCATION ).resolve( "computer" ).resolve( "0" );
|
||||||
{
|
}
|
||||||
Copier.replicate(
|
|
||||||
Minecraft.getInstance().gameDirectory.toPath().resolve( "screenshots" ),
|
private static Path getSourceComputerPath()
|
||||||
TestMod.sourceDir.resolve( "screenshots" ),
|
{
|
||||||
x -> !x.toFile().getName().endsWith( ".diff.png" )
|
return TestHooks.sourceDir.resolve( "computer" );
|
||||||
);
|
|
||||||
}
|
|
||||||
catch( IOException e )
|
|
||||||
{
|
|
||||||
throw new UncheckedIOException( e );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int error( CommandSourceStack source, String message )
|
private static int error( CommandSourceStack source, String message )
|
@ -3,7 +3,7 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* 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.CloudStatus;
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
@ -17,7 +17,7 @@ import net.minecraftforge.fml.common.Mod;
|
|||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
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
|
public final class ClientHooks
|
||||||
{
|
{
|
||||||
private static final Logger LOG = LogManager.getLogger( TestHooks.class );
|
private static final Logger LOG = LogManager.getLogger( TestHooks.class );
|
@ -3,7 +3,7 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* 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.MoreFiles;
|
||||||
import com.google.common.io.RecursiveDeleteOption;
|
import com.google.common.io.RecursiveDeleteOption;
|
@ -3,22 +3,22 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
*/
|
*/
|
||||||
package dan200.computercraft.ingame.mod;
|
package dan200.computercraft.gametest.core;
|
||||||
|
|
||||||
import dan200.computercraft.ComputerCraft;
|
import dan200.computercraft.ComputerCraft;
|
||||||
import dan200.computercraft.api.lua.IComputerSystem;
|
import dan200.computercraft.api.lua.IComputerSystem;
|
||||||
import dan200.computercraft.api.lua.ILuaAPI;
|
import dan200.computercraft.api.lua.ILuaAPI;
|
||||||
import dan200.computercraft.api.lua.LuaException;
|
import dan200.computercraft.api.lua.LuaException;
|
||||||
import dan200.computercraft.api.lua.LuaFunction;
|
import dan200.computercraft.api.lua.LuaFunction;
|
||||||
import dan200.computercraft.ingame.api.ComputerState;
|
import dan200.computercraft.gametest.api.ComputerState;
|
||||||
import dan200.computercraft.ingame.api.TestExtensionsKt;
|
import dan200.computercraft.gametest.api.TestExtensionsKt;
|
||||||
import net.minecraft.gametest.framework.GameTestSequence;
|
import net.minecraft.gametest.framework.GameTestSequence;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API exposed to computers to help write tests.
|
* API exposed to computers to help write tests.
|
||||||
*
|
* <p>
|
||||||
* Note, we extend this API within startup file of computers (see {@code cctest.lua}).
|
* 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.
|
* @see TestExtensionsKt#thenComputerOk(GameTestSequence, String, String) To check tests on the computer have passed.
|
@ -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 );
|
||||||
|
}
|
||||||
|
}
|
144
src/testMod/java/dan200/computercraft/gametest/core/TestMod.java
Normal file
144
src/testMod/java/dan200/computercraft/gametest/core/TestMod.java
Normal 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 );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -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" },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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") }
|
|
||||||
}
|
|
@ -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) }
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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) );
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 );
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
@ -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 )
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -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") }
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
102
src/testMod/kotlin/dan200/computercraft/gametest/Modem_Test.kt
Normal file
102
src/testMod/kotlin/dan200/computercraft/gametest/Modem_Test.kt
Normal 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
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
252
src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt
Normal file
252
src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt
Normal 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, *>
|
||||||
|
}
|
@ -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)))
|
||||||
|
}
|
@ -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`())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
|
@ -16,3 +16,10 @@ displayName="CC: Tweaked test framework"
|
|||||||
description='''
|
description='''
|
||||||
A test framework for ensuring CC: Tweaked works correctly.
|
A test framework for ensuring CC: Tweaked works correctly.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
[[dependencies.cctest]]
|
||||||
|
modId="computercraft"
|
||||||
|
mandatory=true
|
||||||
|
versionRange="[1.0,)"
|
||||||
|
ordering="AFTER"
|
||||||
|
side="BOTH"
|
||||||
|
16
src/testMod/resources/computercraft-gametest.mixins.json
Normal file
16
src/testMod/resources/computercraft-gametest.mixins.json
Normal 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
Loading…
Reference in New Issue
Block a user