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

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

This commit is contained in:
Jonathan Coates 2022-10-30 08:49:52 +00:00
commit 38b2c944f3
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
72 changed files with 1710 additions and 753 deletions

View File

@ -1,9 +1,9 @@
import cc.tweaked.gradle.*
import net.darkhax.curseforgegradle.TaskPublishCurseForge
import net.minecraftforge.gradle.common.util.RunConfig
plugins {
// Build
alias(libs.plugins.kotlin)
alias(libs.plugins.forgeGradle)
alias(libs.plugins.mixinGradle)
alias(libs.plugins.librarian)
@ -18,7 +18,7 @@ plugins {
id("cc-tweaked.illuaminate")
id("cc-tweaked.node")
id("cc-tweaked.java-convention")
id("cc-tweaked.gametest")
id("cc-tweaked")
}
@ -36,8 +36,6 @@ sourceSets {
main {
resources.srcDir("src/generated/resources")
}
register("testMod")
}
minecraft {
@ -80,29 +78,36 @@ minecraft {
property("cct.pretty-json", "true")
}
fun RunConfig.configureForGameTest() {
val old = lazyTokens.get("minecraft_classpath")
lazyToken("minecraft_classpath") {
// We do some terrible hacks here to basically find all things not already on the runtime classpath
// and add them. /Except/ for our source sets, as those need to load inside the Minecraft classpath.
val testMod = configurations["testModRuntimeClasspath"].resolve()
val implementation = configurations.runtimeClasspath.get().resolve()
val new = (testMod - implementation)
.asSequence().filter { it.isFile }.map { it.absolutePath }
.joinToString(File.pathSeparator)
if (old == null) new else old.get() + File.pathSeparator + new
}
mods.register("cctest") {
source(sourceSets["testMod"])
source(sourceSets["testFixtures"])
}
}
val testClient by registering {
workingDirectory(file("run/testClient"))
parent(client.get())
mods.register("cctest") { source(sourceSets["testMod"]) }
lazyToken("minecraft_classpath") {
(configurations["shade"].copyRecursive().resolve() + configurations["testModExtra"].copyRecursive().resolve())
.joinToString(File.pathSeparator) { it.absolutePath }
}
configureForGameTest()
}
val gameTestServer by registering {
workingDirectory(file("run/testServer"))
configureForGameTest()
property("forge.logging.console.level", "info")
mods.register("cctest") { source(sourceSets["testMod"]) }
lazyToken("minecraft_classpath") {
(configurations["shade"].copyRecursive().resolve() + configurations["testModExtra"].copyRecursive().resolve())
.joinToString(File.pathSeparator) { it.absolutePath }
}
}
}
@ -125,9 +130,6 @@ configurations {
val shade by registering { isTransitive = false }
implementation { extendsFrom(shade.get()) }
register("cctJavadoc")
val testModExtra by registering
named("testModImplementation") { extendsFrom(implementation.get(), testModExtra.get()) }
}
dependencies {
@ -143,15 +145,13 @@ dependencies {
"shade"(libs.cobalt)
testFixturesApi(libs.bundles.test)
testFixturesApi(libs.bundles.kotlin)
testImplementation(libs.bundles.test)
testImplementation(libs.bundles.kotlin)
testRuntimeOnly(libs.bundles.testRuntime)
"testModImplementation"(sourceSets.main.get().output)
"testModExtra"(libs.bundles.kotlin) {
exclude("org.jetbrains", "annotations")
}
"cctJavadoc"(libs.cctJavadoc)
}
@ -189,7 +189,7 @@ val luaJavadoc by tasks.registering(Javadoc::class) {
javadocTool.set(
javaToolchains.javadocToolFor {
languageVersion.set(JavaLanguageVersion.of(17))
languageVersion.set(cc.tweaked.gradle.CCTweakedPlugin.JAVA_VERSION)
},
)
}
@ -219,7 +219,7 @@ tasks.jar {
"Specification-Title" to "computercraft",
"Specification-Vendor" to "SquidDev",
"Specification-Version" to "1",
"specificationVersion" to "cctweaked",
"Implementation-Title" to "cctweaked",
"Implementation-Version" to modVersion,
"Implementation-Vendor" to "SquidDev",
)
@ -312,7 +312,7 @@ val docWebsite by tasks.registering(Copy::class) {
// Check tasks
tasks.test {
systemProperty("cct.test-files", buildDir.resolve("tmp/test-files").absolutePath)
systemProperty("cct.test-files", buildDir.resolve("tmp/testFiles").absolutePath)
}
val lintLua by tasks.registering(IlluaminateExec::class) {

View File

@ -9,6 +9,7 @@ repositories {
}
dependencies {
implementation(libs.kotlin.plugin)
implementation(libs.spotless)
}

View File

@ -0,0 +1,53 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
/**
* Sets up the configurations for writing game tests.
*
* See notes in [cc.tweaked.gradle.MinecraftConfigurations] for the general design behind these cursed ideas.
*/
plugins {
id("cc-tweaked.kotlin-convention")
id("cc-tweaked.java-convention")
}
val main = sourceSets.main.get()
// Both testMod and testFixtures inherit from the main classpath, just so we have access to Minecraft classes.
val testMod by sourceSets.creating {
compileClasspath += main.compileClasspath
runtimeClasspath += main.runtimeClasspath
}
configurations {
named(testMod.compileClasspathConfigurationName) {
shouldResolveConsistentlyWith(compileClasspath.get())
}
named(testMod.runtimeClasspathConfigurationName) {
shouldResolveConsistentlyWith(runtimeClasspath.get())
}
}
// Like the main test configurations, we're safe to depend on source set outputs.
dependencies {
add(testMod.implementationConfigurationName, main.output)
}
// Similar to java-test-fixtures, but tries to avoid putting the obfuscated jar on the classpath.
val testFixtures by sourceSets.creating {
compileClasspath += main.compileClasspath
}
java.registerFeature("testFixtures") {
usingSourceSet(testFixtures)
disablePublication()
}
dependencies {
add(testFixtures.implementationConfigurationName, main.output)
testImplementation(testFixtures(project))
add(testMod.implementationConfigurationName, testFixtures(project))
}

View File

@ -1,3 +1,4 @@
import cc.tweaked.gradle.CCTweakedPlugin
import cc.tweaked.gradle.LicenseHeader
import com.diffplug.gradle.spotless.FormatExtension
import com.diffplug.spotless.LineEnding
@ -12,7 +13,7 @@ plugins {
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
languageVersion.set(CCTweakedPlugin.JAVA_VERSION)
}
withSourcesJar()

View File

@ -0,0 +1,21 @@
import cc.tweaked.gradle.CCTweakedPlugin
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm")
}
kotlin {
jvmToolchain {
languageVersion.set(CCTweakedPlugin.JAVA_VERSION)
}
}
tasks.withType(KotlinCompile::class.java).configureEach {
// So technically we shouldn't need to do this as the toolchain sets it above. However, the option only appears
// to be set when the task executes, so doesn't get picked up by IDEs.
kotlinOptions.jvmTarget = when {
CCTweakedPlugin.JAVA_VERSION.asInt() > 8 -> CCTweakedPlugin.JAVA_VERSION.toString()
else -> "1.${CCTweakedPlugin.JAVA_VERSION.asInt()}"
}
}

View File

@ -2,6 +2,7 @@
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.jvm.toolchain.JavaLanguageVersion
/**
* Configures projects to match a shared configuration.
@ -10,4 +11,8 @@ class CCTweakedPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.extensions.create("cct", CCTweakedExtension::class.java)
}
companion object {
val JAVA_VERSION = JavaLanguageVersion.of(17)
}
}

View File

@ -0,0 +1,42 @@
---
module: [kind=event] file_transfer
since: 1.101.0
---
The @{file_transfer} event is queued when a user drags-and-drops a file on an open computer.
This event contains a single argument, that in turn has a single method @{TransferredFiles.getFiles|getFiles}. This
returns the list of files that are being transferred. Each file is a @{fs.BinaryReadHandle|binary file handle} with an
additional @{TransferredFile.getName|getName} method.
## Return values
1. @{string}: The event name
2. @{TransferredFiles}: The list of transferred files.
## Example
Waits for a user to drop files on top of the computer, then prints the list of files and the size of each file.
```lua
local _, files = os.pullEvent("file_transfer")
for _, file in ipairs(files.getFiles()) do
-- Seek to the end of the file to get its size, then go back to the beginning.
local size = file.seek("end")
file.seek("set", 0)
print(file.getName() .. " " .. file.getSize())
end
```
## Example
Save each transferred file to the computer's storage.
```lua
local _, files = os.pullEvent("file_transfer")
for _, file in ipairs(files.getFiles()) do
local handle = fs.open(file.getName(), "wb")
handle.write(file.readAll())
handle.close()
file.close()
end
```

View File

@ -18,12 +18,12 @@ jqwik = "1.7.0"
junit = "5.9.1"
# Build tools
cctJavadoc = "1.5.1"
cctJavadoc = "1.5.2"
checkstyle = "10.3.4"
curseForgeGradle = "1.0.11"
forgeGradle = "5.1.+"
githubRelease = "2.2.12"
illuaminate = "0.1.0-3-g0f40379"
illuaminate = "0.1.0-7-g2a5a89c"
librarian = "1.+"
minotaur = "2.+"
mixinGradle = "0.7.+"
@ -49,6 +49,7 @@ junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", vers
# Build tools
cctJavadoc = { module = "cc.tweaked:cct-javadoc", version.ref = "cctJavadoc" }
checkstyle = { module = "com.puppycrawl.tools:checkstyle", version.ref = "checkstyle" }
kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" }
[plugins]

View File

@ -74,6 +74,8 @@ public final class ComputerCraft
public static int monitorWidth = 8;
public static int monitorHeight = 6;
public static int uploadNagDelay = 5;
public static final Logger log = LoggerFactory.getLogger( MOD_ID );
public ComputerCraft()

View File

@ -17,30 +17,35 @@
import dan200.computercraft.shared.computer.upload.FileUpload;
import dan200.computercraft.shared.computer.upload.UploadResult;
import dan200.computercraft.shared.network.NetworkHandler;
import dan200.computercraft.shared.network.server.ContinueUploadMessage;
import dan200.computercraft.shared.network.server.UploadFileMessage;
import net.minecraft.ChatFormatting;
import net.minecraft.Util;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.TextComponent;
import net.minecraft.network.chat.TranslatableComponent;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.item.ItemStack;
import org.lwjgl.glfw.GLFW;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
public abstract class ComputerScreenBase<T extends ContainerComputerBase> extends AbstractContainerScreen<T>
{
private static final Component OK = new TranslatableComponent( "gui.ok" );
private static final Component CANCEL = new TranslatableComponent( "gui.cancel" );
private static final Component OVERWRITE = new TranslatableComponent( "gui.computercraft.upload.overwrite_button" );
private static final Component NO_RESPONSE_TITLE = new TranslatableComponent( "gui.computercraft.upload.no_response" );
private static final Component NO_RESPONSE_MSG = new TranslatableComponent( "gui.computercraft.upload.no_response.msg",
new TextComponent( "import" ).withStyle( ChatFormatting.DARK_GRAY ) );
protected WidgetTerminal terminal;
protected Terminal terminalData;
@ -49,11 +54,15 @@ public abstract class ComputerScreenBase<T extends ContainerComputerBase> extend
protected final int sidebarYOffset;
private long uploadNagDeadline = Long.MAX_VALUE;
private final ItemStack displayStack;
public ComputerScreenBase( T container, Inventory player, Component title, int sidebarYOffset )
{
super( container, player, title );
terminalData = container.getTerminal();
family = container.getFamily();
displayStack = container.getDisplayStack();
input = new ClientInputHandler( menu );
this.sidebarYOffset = sidebarYOffset;
}
@ -83,6 +92,13 @@ public void containerTick()
{
super.containerTick();
terminal.update();
if( uploadNagDeadline != Long.MAX_VALUE && Util.getNanos() >= uploadNagDeadline )
{
new ItemToast( minecraft, displayStack, NO_RESPONSE_TITLE, NO_RESPONSE_MSG, ItemToast.TRANSFER_NO_RESPONSE_TOKEN )
.showOrReplace( minecraft.getToasts() );
uploadNagDeadline = Long.MAX_VALUE;
}
}
@Override
@ -194,41 +210,29 @@ public void onFilesDrop( @Nonnull List<Path> files )
if( toUpload.size() > 0 ) UploadFileMessage.send( menu, toUpload, NetworkHandler::sendToServer );
}
public void uploadResult( UploadResult result, Component message )
public void uploadResult( UploadResult result, @Nullable Component message )
{
switch( result )
{
case SUCCESS:
alert( UploadResult.SUCCESS_TITLE, message );
case QUEUED:
{
if( ComputerCraft.uploadNagDelay > 0 )
{
uploadNagDeadline = Util.getNanos() + TimeUnit.SECONDS.toNanos( ComputerCraft.uploadNagDelay );
}
break;
}
case CONSUMED:
{
uploadNagDeadline = Long.MAX_VALUE;
break;
}
case ERROR:
alert( UploadResult.FAILED_TITLE, message );
break;
case CONFIRM_OVERWRITE:
OptionScreen.show(
minecraft, UploadResult.UPLOAD_OVERWRITE, message,
Arrays.asList(
OptionScreen.newButton( CANCEL, b -> cancelUpload() ),
OptionScreen.newButton( OVERWRITE, b -> continueUpload() )
),
this::cancelUpload
);
break;
}
}
private void continueUpload()
{
if( minecraft.screen instanceof OptionScreen screen ) screen.disable();
NetworkHandler.sendToServer( new ContinueUploadMessage( menu, true ) );
}
private void cancelUpload()
{
minecraft.setScreen( this );
NetworkHandler.sendToServer( new ContinueUploadMessage( menu, false ) );
}
private void alert( Component title, Component message )
{
OptionScreen.show( minecraft, title, message,

View File

@ -0,0 +1,150 @@
/*
* 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.client.gui;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.components.toasts.Toast;
import net.minecraft.client.gui.components.toasts.ToastComponent;
import net.minecraft.network.chat.Component;
import net.minecraft.util.FormattedCharSequence;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nonnull;
import java.util.List;
/**
* A {@link Toast} implementation which displays an arbitrary message along with an optional {@link ItemStack}.
*/
public class ItemToast implements Toast
{
public static final Object TRANSFER_NO_RESPONSE_TOKEN = new Object();
private static final long DISPLAY_TIME = 7000L;
private static final int MAX_LINE_SIZE = 200;
private static final int IMAGE_SIZE = 16;
private static final int LINE_SPACING = 10;
private static final int MARGIN = 8;
private final ItemStack stack;
private final Component title;
private final List<FormattedCharSequence> message;
private final Object token;
private final int width;
private boolean isNew = true;
private long firstDisplay;
public ItemToast( Minecraft minecraft, ItemStack stack, Component title, Component message, Object token )
{
this.stack = stack;
this.title = title;
this.token = token;
Font font = minecraft.font;
this.message = font.split( message, MAX_LINE_SIZE );
width = Math.max( MAX_LINE_SIZE, this.message.stream().mapToInt( font::width ).max().orElse( MAX_LINE_SIZE ) ) + MARGIN * 3 + IMAGE_SIZE;
}
public void showOrReplace( ToastComponent toasts )
{
ItemToast existing = toasts.getToast( ItemToast.class, getToken() );
if( existing != null )
{
existing.isNew = true;
}
else
{
toasts.addToast( this );
}
}
@Override
public int width()
{
return width;
}
@Override
public int height()
{
return MARGIN * 2 + LINE_SPACING + message.size() * LINE_SPACING;
}
@Nonnull
@Override
public Object getToken()
{
return token;
}
@Nonnull
@Override
public Visibility render( @Nonnull PoseStack transform, @Nonnull ToastComponent component, long time )
{
if( isNew )
{
firstDisplay = time;
isNew = false;
}
RenderSystem.setShaderTexture( 0, TEXTURE );
RenderSystem.setShaderColor( 1.0F, 1.0F, 1.0F, 1.0F );
if( width == 160 && message.size() <= 1 )
{
component.blit( transform, 0, 0, 0, 64, width, height() );
}
else
{
int height = height();
int bottom = Math.min( 4, height - 28 );
renderBackgroundRow( transform, component, width, 0, 0, 28 );
for( int i = 28; i < height - bottom; i += 10 )
{
renderBackgroundRow( transform, component, width, 16, i, Math.min( 16, height - i - bottom ) );
}
renderBackgroundRow( transform, component, width, 32 - bottom, height - bottom, bottom );
}
int textX = MARGIN;
if( !stack.isEmpty() )
{
textX += MARGIN + IMAGE_SIZE;
component.getMinecraft().getItemRenderer().renderAndDecorateFakeItem( stack, MARGIN, MARGIN + height() / 2 - IMAGE_SIZE );
}
component.getMinecraft().font.draw( transform, title, textX, MARGIN, 0xff500050 );
for( int i = 0; i < message.size(); ++i )
{
component.getMinecraft().font.draw( transform, message.get( i ), textX, (float) (LINE_SPACING + (i + 1) * LINE_SPACING), 0xff000000 );
}
return time - firstDisplay < DISPLAY_TIME ? Visibility.SHOW : Visibility.HIDE;
}
private static void renderBackgroundRow( PoseStack transform, ToastComponent component, int x, int u, int y, int height )
{
int leftOffset = 5;
int rightOffset = Math.min( 60, x - leftOffset );
component.blit( transform, 0, y, 0, 32 + u, leftOffset, height );
for( int k = leftOffset; k < x - rightOffset; k += 64 )
{
component.blit( transform, k, y, 32, 32 + u, Math.min( 64, x - k - rightOffset ), height );
}
component.blit( transform, x - rightOffset, y, 160 - rightOffset, 32 + u, rightOffset, height );
}
}

View File

@ -485,7 +485,9 @@ public final Object date( Optional<String> formatA, Optional<Long> timeA ) throw
DateTimeFormatterBuilder formatter = new DateTimeFormatterBuilder();
LuaDateTime.format( formatter, format );
return formatter.toFormatter( Locale.ROOT ).format( date );
// ROOT would be more sensible, but US appears more consistent with the default C locale
// on Linux.
return formatter.toFormatter( Locale.US ).format( date );
}
}

View File

@ -0,0 +1,99 @@
/*
* 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.handles;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.NonWritableChannelException;
import java.nio.channels.SeekableByteChannel;
import java.util.Objects;
/**
* A seekable, readable byte channel which is backed by a {@link ByteBuffer}.
*/
public class ByteBufferChannel implements SeekableByteChannel
{
private boolean closed = false;
private int position = 0;
private final ByteBuffer backing;
public ByteBufferChannel( ByteBuffer backing )
{
this.backing = backing;
}
@Override
public int read( ByteBuffer destination ) throws ClosedChannelException
{
if( closed ) throw new ClosedChannelException();
Objects.requireNonNull( destination, "destination" );
if( position >= backing.limit() ) return -1;
int remaining = Math.min( backing.limit() - position, destination.remaining() );
// TODO: Switch to Java 17 methods on 1.18.x
ByteBuffer slice = backing.slice();
slice.position( position );
slice.limit( position + remaining );
destination.put( slice );
position += remaining;
return remaining;
}
@Override
public int write( ByteBuffer src ) throws ClosedChannelException
{
if( closed ) throw new ClosedChannelException();
throw new NonWritableChannelException();
}
@Override
public long position() throws ClosedChannelException
{
if( closed ) throw new ClosedChannelException();
return position;
}
@Override
public SeekableByteChannel position( long newPosition ) throws ClosedChannelException
{
if( closed ) throw new ClosedChannelException();
if( newPosition < 0 || newPosition > Integer.MAX_VALUE )
{
throw new IllegalArgumentException( "Position out of bounds" );
}
position = (int) newPosition;
return this;
}
@Override
public long size() throws ClosedChannelException
{
if( closed ) throw new ClosedChannelException();
return backing.limit();
}
@Override
public SeekableByteChannel truncate( long size ) throws ClosedChannelException
{
if( closed ) throw new ClosedChannelException();
throw new NonWritableChannelException();
}
@Override
public boolean isOpen()
{
return !closed;
}
@Override
public void close()
{
closed = true;
}
}

View File

@ -5,6 +5,7 @@
*/
package dan200.computercraft.core.computer;
import com.google.common.annotations.VisibleForTesting;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.core.ComputerContext;
@ -443,7 +444,8 @@ long scaledPeriod()
*
* @return If we have work queued up.
*/
boolean hasPendingWork()
@VisibleForTesting
public boolean hasPendingWork()
{
// FIXME: See comment in scaledPeriod. Again, we access this in multiple threads but not clear if it matters!
return !computerQueue.isEmpty();

View File

@ -295,6 +295,7 @@ protected Void prepare( @Nonnull ResourceManager manager, @Nonnull ProfilerFille
try
{
for( ResourceMount mount : MOUNT_CACHE.values() ) mount.load( manager );
CONTENTS_CACHE.invalidateAll();
}
finally
{

View File

@ -16,7 +16,6 @@
import dan200.computercraft.core.apis.http.options.AddressRuleConfig;
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
import net.minecraftforge.common.ForgeConfigSpec;
import net.minecraftforge.common.ForgeConfigSpec.Builder;
import net.minecraftforge.common.ForgeConfigSpec.ConfigValue;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.ModLoadingContext;
@ -83,6 +82,7 @@ public final class Config
private static final ConfigValue<MonitorRenderer> monitorRenderer;
private static final ConfigValue<Integer> monitorDistance;
private static final ConfigValue<Integer> uploadNagDelay;
private static final ForgeConfigSpec serverSpec;
private static final ForgeConfigSpec clientSpec;
@ -91,7 +91,7 @@ private Config() {}
static
{
Builder builder = new Builder();
ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder();
{ // General computers
computerSpaceLimit = builder
@ -275,13 +275,17 @@ private Config() {}
serverSpec = builder.build();
Builder clientBuilder = new Builder();
ForgeConfigSpec.Builder clientBuilder = new ForgeConfigSpec.Builder();
monitorRenderer = clientBuilder
.comment( "The renderer to use for monitors. Generally this should be kept at \"best\" - if\nmonitors have performance issues, you may wish to experiment with alternative\nrenderers." )
.defineEnum( "monitor_renderer", MonitorRenderer.BEST );
monitorDistance = clientBuilder
.comment( "The maximum distance monitors will render at. This defaults to the standard tile\nentity limit, but may be extended if you wish to build larger monitors." )
.defineInRange( "monitor_distance", 64, 16, 1024 );
uploadNagDelay = clientBuilder
.comment( "The delay in seconds after which we'll notify about unhandled imports. Set to 0 to disable." )
.defineInRange( "upload_nag_delay", ComputerCraft.uploadNagDelay, 0, 60 );
clientSpec = clientBuilder.build();
}
@ -347,6 +351,7 @@ public static void sync()
// Client
ComputerCraft.monitorRenderer = monitorRenderer.get();
ComputerCraft.monitorDistance = monitorDistance.get();
ComputerCraft.uploadNagDelay = uploadNagDelay.get();
}
@SubscribeEvent

View File

@ -35,6 +35,7 @@
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import javax.annotation.Nonnull;
@ -224,7 +225,7 @@ else if( b.getLevel() == world )
.executes( context -> {
ServerPlayer player = context.getSource().getPlayerOrException();
ServerComputer computer = getComputerArgument( context, "computer" );
new ComputerContainerData( computer ).open( player, new MenuProvider()
new ComputerContainerData( computer, ItemStack.EMPTY ).open( player, new MenuProvider()
{
@Nonnull
@Override

View File

@ -19,17 +19,9 @@
import net.minecraft.world.phys.BlockHitResult;
import javax.annotation.Nonnull;
import java.util.concurrent.atomic.AtomicBoolean;
public abstract class TileGeneric extends BlockEntity
{
/**
* Is this block enqueued to be updated next tick? This should only be read/written by the tick scheduler.
*
* @see dan200.computercraft.shared.util.TickScheduler
*/
public final AtomicBoolean scheduled = new AtomicBoolean();
public TileGeneric( BlockEntityType<? extends TileGeneric> type, BlockPos pos, BlockState state )
{
super( type, pos, state );

View File

@ -141,7 +141,11 @@ else if( !player.isCrouching() )
{
ServerComputer computer = createServerComputer();
computer.turnOn();
new ComputerContainerData( computer ).open( player, this );
ItemStack stack = getBlockState().getBlock() instanceof BlockComputerBase<?>
? ((BlockComputerBase<?>) getBlockState().getBlock()).getItem( this )
: ItemStack.EMPTY;
new ComputerContainerData( computer, stack ).open( player, this );
}
return InteractionResult.SUCCESS;
}

View File

@ -9,6 +9,7 @@
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.computer.menu.ServerInputHandler;
import dan200.computercraft.shared.computer.menu.ServerInputState;
import dan200.computercraft.shared.network.client.TerminalState;
import dan200.computercraft.shared.network.container.ComputerContainerData;
@ -18,6 +19,7 @@
import net.minecraft.world.inventory.ContainerData;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.inventory.SimpleContainerData;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@ -30,10 +32,12 @@ public abstract class ContainerComputerBase extends AbstractContainerMenu implem
private final ContainerData data;
private final @Nullable ServerComputer computer;
private final @Nullable ServerInputState input;
private final @Nullable ServerInputState<ContainerComputerBase> input;
private final @Nullable Terminal terminal;
private final ItemStack displayStack;
public ContainerComputerBase(
MenuType<? extends ContainerComputerBase> type, int id, Predicate<Player> canUse,
ComputerFamily family, @Nullable ServerComputer computer, @Nullable ComputerContainerData containerData
@ -46,8 +50,9 @@ public ContainerComputerBase(
addDataSlots( data );
this.computer = computer;
input = computer == null ? null : new ServerInputState( this );
input = computer == null ? null : new ServerInputState<>( this );
terminal = containerData == null ? null : containerData.terminal().create();
displayStack = containerData == null ? null : containerData.displayStack();
}
@Override
@ -75,7 +80,7 @@ public ServerComputer getComputer()
}
@Override
public ServerInputState getInput()
public ServerInputHandler getInput()
{
if( input == null ) throw new UnsupportedOperationException( "Cannot access server computer on the client" );
return input;
@ -106,4 +111,15 @@ public void removed( @Nonnull Player player )
super.removed( player );
if( input != null ) input.close();
}
/**
* Get the stack associated with this container.
*
* @return The current stack.
*/
@Nonnull
public ItemStack getDisplayStack()
{
return displayStack;
}
}

View File

@ -47,12 +47,4 @@ public interface ServerInputHandler extends InputHandler
* @param uploadId The unique ID of this upload.
*/
void finishUpload( @Nonnull ServerPlayer uploader, @Nonnull UUID uploadId );
/**
* Continue an upload.
*
* @param uploader The player uploading files.
* @param overwrite Whether the files should be overwritten or not.
*/
void confirmUpload( @Nonnull ServerPlayer uploader, boolean overwrite );
}

View File

@ -6,13 +6,8 @@
package dan200.computercraft.shared.computer.menu;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.core.filesystem.FileSystem;
import dan200.computercraft.core.filesystem.FileSystemException;
import dan200.computercraft.core.filesystem.FileSystemWrapper;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.upload.FileSlice;
import dan200.computercraft.shared.computer.upload.FileUpload;
import dan200.computercraft.shared.computer.upload.UploadResult;
import dan200.computercraft.shared.computer.upload.*;
import dan200.computercraft.shared.network.NetworkHandler;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.client.UploadResultMessage;
@ -21,26 +16,23 @@
import it.unimi.dsi.fastutil.ints.IntSet;
import net.minecraft.network.chat.TranslatableComponent;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.inventory.AbstractContainerMenu;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.StringJoiner;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* The default concrete implementation of {@link ServerInputHandler}.
* <p>
* This keeps track of the current key and mouse state, and releases them when the container is closed.
*
* @param <T> The type of container this server input belongs to.
*/
public class ServerInputState implements ServerInputHandler
public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> implements ServerInputHandler
{
private static final String LIST_PREFIX = "\n \u2022 ";
private final ComputerMenu owner;
private final T owner;
private final IntSet keysDown = new IntOpenHashSet( 4 );
private int lastMouseX;
@ -50,7 +42,7 @@ public class ServerInputState implements ServerInputHandler
private @Nullable UUID toUploadId;
private @Nullable List<FileUpload> toUpload;
public ServerInputState( ComputerMenu owner )
public ServerInputState( T owner )
{
this.owner = owner;
}
@ -160,91 +152,31 @@ public void finishUpload( ServerPlayer uploader, UUID uploadId )
return;
}
NetworkMessage message = finishUpload( false );
NetworkMessage message = finishUpload( uploader );
NetworkHandler.sendToPlayer( uploader, message );
}
@Override
public void confirmUpload( ServerPlayer uploader, boolean overwrite )
{
if( toUploadId == null || toUpload == null || toUpload.isEmpty() )
{
ComputerCraft.log.warn( "Invalid finishUpload call, skipping." );
return;
}
NetworkMessage message = finishUpload( true );
NetworkHandler.sendToPlayer( uploader, message );
}
private UploadResultMessage finishUpload( boolean forceOverwrite )
private UploadResultMessage finishUpload( ServerPlayer player )
{
ServerComputer computer = owner.getComputer();
if( toUpload == null ) return UploadResultMessage.COMPUTER_OFF;
FileSystem fs = computer.getComputer().getAPIEnvironment().getFileSystem();
if( toUpload == null )
{
return UploadResultMessage.error( owner, UploadResult.COMPUTER_OFF_MSG );
}
for( FileUpload upload : toUpload )
{
if( !upload.checksumMatches() )
{
ComputerCraft.log.warn( "Checksum failed to match for {}.", upload.getName() );
return new UploadResultMessage( UploadResult.ERROR, new TranslatableComponent( "gui.computercraft.upload.failed.corrupted" ) );
return UploadResultMessage.error( owner, new TranslatableComponent( "gui.computercraft.upload.failed.corrupted" ) );
}
}
try
{
List<String> overwrite = new ArrayList<>();
List<FileUpload> files = toUpload;
toUpload = null;
for( FileUpload upload : files )
{
if( !fs.exists( upload.getName() ) ) continue;
if( fs.isDir( upload.getName() ) )
{
return new UploadResultMessage(
UploadResult.ERROR,
new TranslatableComponent( "gui.computercraft.upload.failed.overwrite_dir", upload.getName() )
);
}
overwrite.add( upload.getName() );
}
if( !overwrite.isEmpty() && !forceOverwrite )
{
StringJoiner joiner = new StringJoiner( LIST_PREFIX, LIST_PREFIX, "" );
for( String value : overwrite ) joiner.add( value );
toUpload = files;
return new UploadResultMessage(
UploadResult.CONFIRM_OVERWRITE,
new TranslatableComponent( "gui.computercraft.upload.overwrite.detail", joiner.toString() )
);
}
long availableSpace = fs.getFreeSpace( "/" );
long neededSpace = 0;
for( FileUpload upload : files ) neededSpace += Math.max( 512, upload.getBytes().remaining() );
if( neededSpace > availableSpace ) return UploadResultMessage.OUT_OF_SPACE;
for( FileUpload file : files )
{
try( FileSystemWrapper<WritableByteChannel> channel = fs.openForWrite( file.getName(), false, Function.identity() ) )
{
channel.get().write( file.getBytes() );
}
}
return new UploadResultMessage(
UploadResult.SUCCESS, new TranslatableComponent( "gui.computercraft.upload.success.msg", files.size() )
);
}
catch( FileSystemException | IOException e )
{
ComputerCraft.log.error( "Error uploading files", e );
return new UploadResultMessage( UploadResult.ERROR, new TranslatableComponent( "gui.computercraft.upload.failed.generic", e.getMessage() ) );
}
computer.queueEvent( "file_transfer", new Object[] {
new TransferredFiles( player, owner, toUpload.stream().map( x -> new TransferredFile( x.getName(), x.getBytes() ) ).collect( Collectors.toList() ) ),
} );
return UploadResultMessage.queued( owner );
}
public void close()

View File

@ -0,0 +1,53 @@
/*
* 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.upload;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
import dan200.computercraft.core.apis.handles.ByteBufferChannel;
import dan200.computercraft.core.asm.ObjectSource;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.Optional;
/**
* A binary file handle that has been transferred to this computer.
* <p>
* This inherits all methods of {@link BinaryReadableHandle binary file handles}, meaning you can use the standard
* {@link BinaryReadableHandle#read(Optional) read functions} to access the contents of the file.
*
* @cc.module [kind=event] file_transfer.TransferredFile
* @see BinaryReadableHandle
*/
public class TransferredFile implements ObjectSource
{
private final String name;
private final BinaryReadableHandle handle;
public TransferredFile( String name, ByteBuffer contents )
{
this.name = name;
handle = BinaryReadableHandle.of( new ByteBufferChannel( contents ) );
}
/**
* Get the name of this file being transferred.
*
* @return The file's name.
*/
@LuaFunction
public final String getName()
{
return name;
}
@Override
public Iterable<Object> getExtra()
{
return Collections.singleton( handle );
}
}

View File

@ -0,0 +1,58 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.shared.computer.upload;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.shared.network.NetworkHandler;
import dan200.computercraft.shared.network.client.UploadResultMessage;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.inventory.AbstractContainerMenu;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A list of files that have been transferred to this computer.
*
* @cc.module [kind=event] file_transfer.TransferredFiles
*/
public class TransferredFiles
{
private final ServerPlayer player;
private final AbstractContainerMenu container;
private final AtomicBoolean consumed = new AtomicBoolean( false );
private final List<TransferredFile> files;
public TransferredFiles( ServerPlayer player, AbstractContainerMenu container, List<TransferredFile> files )
{
this.player = player;
this.container = container;
this.files = files;
}
/**
* All the files that are being transferred to this computer.
*
* @return The list of files.
*/
@LuaFunction
public final List<TransferredFile> getFiles()
{
consumed();
return files;
}
private void consumed()
{
if( consumed.getAndSet( true ) ) return;
if( player.isAlive() && player.containerMenu == container )
{
NetworkHandler.sendToPlayer( player, UploadResultMessage.consumed( container ) );
}
}
}

View File

@ -10,16 +10,13 @@
public enum UploadResult
{
SUCCESS,
ERROR,
CONFIRM_OVERWRITE;
QUEUED,
CONSUMED,
ERROR;
public static final Component SUCCESS_TITLE = new TranslatableComponent( "gui.computercraft.upload.success" );
public static final Component FAILED_TITLE = new TranslatableComponent( "gui.computercraft.upload.failed" );
public static final Component COMPUTER_OFF_MSG = new TranslatableComponent( "gui.computercraft.upload.failed.computer_off" );
public static final Component OUT_OF_SPACE_MSG = new TranslatableComponent( "gui.computercraft.upload.failed.out_of_space" );
public static final Component TOO_MUCH_MSG = new TranslatableComponent( "gui.computercraft.upload.failed.too_much" );
public static final Component UPLOAD_OVERWRITE = new TranslatableComponent( "gui.computercraft.upload.overwrite" );
}

View File

@ -50,7 +50,6 @@ public static void setup()
registerMainThread( 2, NetworkDirection.PLAY_TO_SERVER, KeyEventServerMessage.class, KeyEventServerMessage::new );
registerMainThread( 3, NetworkDirection.PLAY_TO_SERVER, MouseEventServerMessage.class, MouseEventServerMessage::new );
registerMainThread( 4, NetworkDirection.PLAY_TO_SERVER, UploadFileMessage.class, UploadFileMessage::new );
registerMainThread( 5, NetworkDirection.PLAY_TO_SERVER, ContinueUploadMessage.class, ContinueUploadMessage::new );
// Client messages
registerMainThread( 10, NetworkDirection.PLAY_TO_CLIENT, ChatTableClientMessage.class, ChatTableClientMessage::new );

View File

@ -13,35 +13,53 @@
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraftforge.network.NetworkEvent;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class UploadResultMessage implements NetworkMessage
{
public static final UploadResultMessage COMPUTER_OFF = new UploadResultMessage( UploadResult.ERROR, UploadResult.COMPUTER_OFF_MSG );
public static final UploadResultMessage OUT_OF_SPACE = new UploadResultMessage( UploadResult.ERROR, UploadResult.OUT_OF_SPACE_MSG );
private final int containerId;
private final UploadResult result;
private final Component message;
private final Component errorMessage;
public UploadResultMessage( UploadResult result, Component message )
private UploadResultMessage( AbstractContainerMenu container, UploadResult result, @Nullable Component errorMessage )
{
containerId = container.containerId;
this.result = result;
this.message = message;
this.errorMessage = errorMessage;
}
public static UploadResultMessage queued( AbstractContainerMenu container )
{
return new UploadResultMessage( container, UploadResult.QUEUED, null );
}
public static UploadResultMessage consumed( AbstractContainerMenu container )
{
return new UploadResultMessage( container, UploadResult.CONSUMED, null );
}
public static UploadResultMessage error( AbstractContainerMenu container, Component errorMessage )
{
return new UploadResultMessage( container, UploadResult.ERROR, errorMessage );
}
public UploadResultMessage( @Nonnull FriendlyByteBuf buf )
{
containerId = buf.readVarInt();
result = buf.readEnum( UploadResult.class );
message = buf.readComponent();
errorMessage = result == UploadResult.ERROR ? buf.readComponent() : null;
}
@Override
public void toBytes( @Nonnull FriendlyByteBuf buf )
{
buf.writeVarInt( containerId );
buf.writeEnum( result );
buf.writeComponent( message );
if( result == UploadResult.ERROR ) buf.writeComponent( errorMessage );
}
@Override
@ -50,9 +68,9 @@ public void handle( NetworkEvent.Context context )
Minecraft minecraft = Minecraft.getInstance();
Screen screen = OptionScreen.unwrap( minecraft.screen );
if( screen instanceof ComputerScreenBase<?> )
if( screen instanceof ComputerScreenBase<?> && ((ComputerScreenBase<?>) screen).getMenu().containerId == containerId )
{
((ComputerScreenBase<?>) screen).uploadResult( result, message );
((ComputerScreenBase<?>) screen).uploadResult( result, errorMessage );
}
}
}

View File

@ -9,22 +9,28 @@
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.network.client.TerminalState;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nonnull;
public class ComputerContainerData implements ContainerData
{
private final ComputerFamily family;
private final TerminalState terminal;
private final ItemStack displayStack;
public ComputerContainerData( ServerComputer computer )
public ComputerContainerData( ServerComputer computer, @Nonnull ItemStack displayStack )
{
family = computer.getFamily();
terminal = computer.getTerminalState();
this.displayStack = displayStack;
}
public ComputerContainerData( FriendlyByteBuf buf )
{
family = buf.readEnum( ComputerFamily.class );
terminal = new TerminalState( buf );
displayStack = buf.readItem();
}
@Override
@ -32,6 +38,7 @@ public void toBytes( FriendlyByteBuf buf )
{
buf.writeEnum( family );
terminal.write( buf );
buf.writeItemStack( displayStack, true );
}
public ComputerFamily family()
@ -43,4 +50,15 @@ public TerminalState terminal()
{
return terminal;
}
/**
* Get a stack associated with this menu. This may be displayed on the client.
*
* @return The stack associated with this menu.
*/
@Nonnull
public ItemStack displayStack()
{
return displayStack;
}
}

View File

@ -1,45 +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.shared.network.server;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraftforge.network.NetworkEvent;
import javax.annotation.Nonnull;
public class ContinueUploadMessage extends ComputerServerMessage
{
private final boolean overwrite;
public ContinueUploadMessage( AbstractContainerMenu menu, boolean overwrite )
{
super( menu );
this.overwrite = overwrite;
}
public ContinueUploadMessage( @Nonnull FriendlyByteBuf buf )
{
super( buf );
overwrite = buf.readBoolean();
}
@Override
public void toBytes( @Nonnull FriendlyByteBuf buf )
{
super.toBytes( buf );
buf.writeBoolean( overwrite );
}
@Override
protected void handle( NetworkEvent.Context context, @Nonnull ComputerMenu container )
{
ServerPlayer player = context.getSender();
if( player != null ) container.getInput().confirmUpload( player, overwrite );
}
}

View File

@ -87,8 +87,9 @@ protected void detachPeripheral( String name )
private final WiredModemElement cable = new CableElement();
private LazyOptional<IWiredElement> elementCap;
private final IWiredNode node = cable.getNode();
private final TickScheduler.Token tickToken = new TickScheduler.Token( this );
private final WiredModemPeripheral modem = new WiredModemPeripheral(
new ModemState( () -> TickScheduler.schedule( this ) ),
new ModemState( () -> TickScheduler.schedule( tickToken ) ),
cable
)
{
@ -168,7 +169,7 @@ public void invalidateCaps()
public void clearRemoved()
{
super.clearRemoved(); // TODO: Replace with onLoad
TickScheduler.schedule( this );
TickScheduler.schedule( tickToken );
}
@Override
@ -241,7 +242,7 @@ private void queueRefreshPeripheral()
{
if( invalidPeripheral ) return;
invalidPeripheral = true;
TickScheduler.schedule( this );
TickScheduler.schedule( tickToken );
}
private void refreshPeripheral()

View File

@ -100,7 +100,8 @@ public Vec3 getPosition()
private boolean destroyed = false;
private boolean connectionsFormed = false;
private final ModemState modemState = new ModemState( () -> TickScheduler.schedule( this ) );
private final TickScheduler.Token tickToken = new TickScheduler.Token( this );
private final ModemState modemState = new ModemState( () -> TickScheduler.schedule( tickToken ) );
private final WiredModemElement element = new FullElement( this );
private LazyOptional<IWiredElement> elementCap;
private final IWiredNode node = element.getNode();
@ -181,7 +182,7 @@ public void onNeighbourTileEntityChange( @Nonnull BlockPos neighbour )
private void queueRefreshPeripheral( @Nonnull Direction facing )
{
if( invalidSides == 0 ) TickScheduler.schedule( this );
if( invalidSides == 0 ) TickScheduler.schedule( tickToken );
invalidSides |= 1 << facing.ordinal();
}
@ -262,7 +263,7 @@ private void updateBlockState()
public void clearRemoved()
{
super.clearRemoved(); // TODO: Replace with onLoad
TickScheduler.schedule( this );
TickScheduler.schedule( tickToken );
}
@Override

View File

@ -72,6 +72,13 @@ public Level getLevel()
protected abstract WiredModemLocalPeripheral getLocalPeripheral();
//endregion
@Nonnull
@Override
public Set<String> getAdditionalTypes()
{
return Collections.singleton( "peripheral_hub" );
}
//region Peripheral methods
/**

View File

@ -33,7 +33,7 @@ private static class Peripheral extends WirelessModemPeripheral
Peripheral( TileWirelessModem entity )
{
super( new ModemState( () -> TickScheduler.schedule( entity ) ), entity.advanced );
super( new ModemState( () -> TickScheduler.schedule( entity.tickToken ) ), entity.advanced );
this.entity = entity;
}
@ -70,6 +70,7 @@ public Object getTarget()
private final ModemPeripheral modem;
private boolean destroyed = false;
private LazyOptional<IPeripheral> modemCap;
private final TickScheduler.Token tickToken = new TickScheduler.Token( this );
public TileWirelessModem( BlockEntityType<? extends TileWirelessModem> type, BlockPos pos, BlockState state, boolean advanced )
{
@ -82,7 +83,7 @@ public TileWirelessModem( BlockEntityType<? extends TileWirelessModem> type, Blo
public void clearRemoved()
{
super.clearRemoved(); // TODO: Replace with onLoad
TickScheduler.schedule( this );
TickScheduler.schedule( tickToken );
}
@Override

View File

@ -64,7 +64,7 @@ synchronized void rebuild()
private void markChanged()
{
if( !changed.getAndSet( true ) ) TickScheduler.schedule( origin );
if( !changed.getAndSet( true ) ) TickScheduler.schedule( origin.tickToken );
}
int getTextScale()

View File

@ -75,6 +75,8 @@ public class TileMonitor extends TileGeneric
private int bbX, bbY, bbWidth, bbHeight;
private AABB boundingBox;
TickScheduler.Token tickToken = new TickScheduler.Token( this );
public TileMonitor( BlockEntityType<? extends TileMonitor> type, BlockPos pos, BlockState state, boolean advanced )
{
super( type, pos, state );
@ -86,7 +88,7 @@ public void clearRemoved() // TODO: Switch back to onLood
{
super.clearRemoved();
needsValidating = true; // Same, tbh
TickScheduler.schedule( this );
TickScheduler.schedule( tickToken );
}
@Override

View File

@ -165,7 +165,7 @@ public InteractionResultHolder<ItemStack> use( Level world, Player player, @Nonn
if( !stop )
{
boolean isTypingOnly = hand == InteractionHand.OFF_HAND;
new ComputerContainerData( computer ).open( player, new PocketComputerMenuProvider( computer, stack, this, hand, isTypingOnly ) );
new ComputerContainerData( computer, stack ).open( player, new PocketComputerMenuProvider( computer, stack, this, hand, isTypingOnly ) );
}
}
return new InteractionResultHolder<>( InteractionResult.SUCCESS, stack );

View File

@ -6,17 +6,18 @@
package dan200.computercraft.shared.util;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.shared.common.TileGeneric;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A thread-safe version of {@link LevelAccessor#scheduleTick(BlockPos, Block, int)}.
@ -30,12 +31,12 @@ private TickScheduler()
{
}
private static final Queue<TileGeneric> toTick = new ConcurrentLinkedDeque<>();
private static final Queue<Token> toTick = new ConcurrentLinkedDeque<>();
public static void schedule( TileGeneric tile )
public static void schedule( Token token )
{
Level world = tile.getLevel();
if( world != null && !world.isClientSide && !tile.scheduled.getAndSet( true ) ) toTick.add( tile );
Level world = token.owner.getLevel();
if( world != null && !world.isClientSide && !token.scheduled.getAndSet( true ) ) toTick.add( token );
}
@SubscribeEvent
@ -43,19 +44,37 @@ public static void tick( TickEvent.ServerTickEvent event )
{
if( event.phase != TickEvent.Phase.START ) return;
TileGeneric tile;
while( (tile = toTick.poll()) != null )
Token token;
while( (token = toTick.poll()) != null )
{
tile.scheduled.set( false );
if( tile.isRemoved() ) continue;
token.scheduled.set( false );
BlockEntity blockEntity = token.owner;
if( blockEntity.isRemoved() ) continue;
Level world = tile.getLevel();
BlockPos pos = tile.getBlockPos();
Level world = blockEntity.getLevel();
BlockPos pos = blockEntity.getBlockPos();
if( world != null && pos != null && world.isLoaded( pos ) && world.getBlockEntity( pos ) == tile )
if( world != null && world.isLoaded( pos ) && world.getBlockEntity( pos ) == blockEntity )
{
world.scheduleTick( pos, tile.getBlockState().getBlock(), 0 );
world.scheduleTick( pos, blockEntity.getBlockState().getBlock(), 0 );
}
}
}
/**
* An item which can be scheduled for future ticking.
* <p>
* This tracks whether the {@link BlockEntity} is queued or not, as this is more efficient than maintaining a set.
* As such, it should be unique per {@link BlockEntity} instance to avoid it being queued multiple times.
*/
public static class Token
{
final BlockEntity owner;
final AtomicBoolean scheduled = new AtomicBoolean();
public Token( BlockEntity owner )
{
this.owner = owner;
}
}
}

View File

@ -119,16 +119,13 @@
"gui.computercraft.upload.success": "Upload Succeeded",
"gui.computercraft.upload.success.msg": "%d files uploaded.",
"gui.computercraft.upload.failed": "Upload Failed",
"gui.computercraft.upload.failed.out_of_space": "Not enough space on the computer for these 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.name_too_long": "File names are too long to be uploaded.",
"gui.computercraft.upload.failed.too_many_files": "Cannot upload this many files.",
"gui.computercraft.upload.failed.overwrite_dir": "Cannot upload %s, as there is already a directory with the same name.",
"gui.computercraft.upload.failed.generic": "Uploading files failed (%s)",
"gui.computercraft.upload.failed.corrupted": "Files corrupted when uploading. Please try again.",
"gui.computercraft.upload.overwrite": "Files would be overwritten",
"gui.computercraft.upload.overwrite.detail": "The following files will be overwritten when uploading. Continue?%s",
"gui.computercraft.upload.overwrite_button": "Overwrite",
"gui.computercraft.upload.no_response": "Transferring Files",
"gui.computercraft.upload.no_response.msg": "Your computer has not used your transferred files. You may need to run the %s program and try again.",
"gui.computercraft.pocket_computer_overlay": "Pocket computer open. Press ESC to close."
}

View File

@ -109,7 +109,7 @@ function getNames()
local side = sides[n]
if native.isPresent(side) then
table.insert(results, side)
if native.hasType(side, "modem") and not native.call(side, "isWireless") then
if native.hasType(side, "peripheral_hub") then
local remote = native.call(side, "getNamesRemote")
for _, name in ipairs(remote) do
table.insert(results, name)
@ -134,9 +134,7 @@ function isPresent(name)
for n = 1, #sides do
local side = sides[n]
if native.hasType(side, "modem") and not native.call(side, "isWireless") and
native.call(side, "isPresentRemote", name)
then
if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", name) then
return true
end
end
@ -162,9 +160,7 @@ function getType(peripheral)
end
for n = 1, #sides do
local side = sides[n]
if native.hasType(side, "modem") and not native.call(side, "isWireless") and
native.call(side, "isPresentRemote", peripheral)
then
if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", peripheral) then
return native.call(side, "getTypeRemote", peripheral)
end
end
@ -195,9 +191,7 @@ function hasType(peripheral, peripheral_type)
end
for n = 1, #sides do
local side = sides[n]
if native.hasType(side, "modem") and not native.call(side, "isWireless") and
native.call(side, "isPresentRemote", peripheral)
then
if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", peripheral) then
return native.call(side, "hasTypeRemote", peripheral, peripheral_type)
end
end
@ -223,9 +217,7 @@ function getMethods(name)
end
for n = 1, #sides do
local side = sides[n]
if native.hasType(side, "modem") and not native.call(side, "isWireless") and
native.call(side, "isPresentRemote", name)
then
if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", name) then
return native.call(side, "getMethodsRemote", name)
end
end
@ -265,9 +257,7 @@ function call(name, method, ...)
for n = 1, #sides do
local side = sides[n]
if native.hasType(side, "modem") and not native.call(side, "isWireless") and
native.call(side, "isPresentRemote", name)
then
if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", name) then
return native.call(side, "callRemote", name, method, ...)
end
end

View File

@ -211,6 +211,8 @@ local function tabulateCommon(bPaged, ...)
end
print()
end
local previous_colour = term.getTextColour()
for _, t in ipairs(tAll) do
if type(t) == "table" then
if #t > 0 then
@ -220,6 +222,7 @@ local function tabulateCommon(bPaged, ...)
term.setTextColor(t)
end
end
term.setTextColor(previous_colour)
end
--[[- Prints tables in a structured form.
@ -685,6 +688,7 @@ do
@treturn[2] nil If the object could not be deserialised.
@treturn string A message describing why the JSON string is invalid.
@since 1.87.0
@changed 1.100.6 Added `parse_empty_array` option
@see textutils.json_null Use to serialize a JSON `null` value.
@see textutils.empty_json_array Use to serialize a JSON empty array.
@usage Unserialise a basic JSON object

View File

@ -0,0 +1,70 @@
-- Internal module for handling file uploads. This has NO stability guarantees,
-- and so SHOULD NOT be relyed on in user code.
local completion = require "cc.completion"
return function(files)
local overwrite = {}
for _, file in pairs(files) do
local filename = file.getName()
local path = shell.resolve(filename)
if fs.exists(path) then
if fs.isDir(path) then
return nil, filename .. " is already a directory."
end
overwrite[#overwrite + 1] = filename
end
end
if #overwrite > 0 then
table.sort(overwrite)
printError("The following files will be overwritten:")
textutils.pagedTabulate(colours.cyan, overwrite)
while true do
io.write("Overwrite? (yes/no) ")
local input = read(nil, nil, function(t)
return completion.choice(t, { "yes", "no" })
end)
if not input then return end
input = input:lower()
if input == "" or input == "yes" or input == "y" then
break
elseif input == "no" or input == "n" then
return
end
end
end
for _, file in pairs(files) do
local filename = file.getName()
print("Transferring " .. filename)
local path = shell.resolve(filename)
local handle, err = fs.open(path, "wb")
if not handle then return nil, err end
-- Write the file without loading it all into memory. This uses the same buffer size
-- as BinaryReadHandle. It would be really nice to have a way to do this without
-- multiple copies.
while true do
local chunk = file.read(8192)
if not chunk then break end
local ok, err = pcall(handle.write, chunk)
if not ok then
handle.close()
-- Probably an out-of-space issue, just bail.
if err:sub(1, 7) == "pcall: " then err = err:sub(8) end
return nil, "Failed to write file (" .. err .. "). File may be corrupted"
end
end
handle.close()
end
return true
end

View File

@ -29,6 +29,7 @@ application or development builds of [FFmpeg].
@see speaker.playAudio To play the decoded audio data.
@usage Reads "data/example.dfpwm" in chunks, decodes them and then doubles the speed of the audio. The resulting audio
is then re-encoded and saved to "speedy.dfpwm". This processed audio can then be played with the `speaker` program.
@since 1.100.0
```lua
local dfpwm = require("cc.audio.dfpwm")

View File

@ -331,9 +331,8 @@ while #tProcesses > 0 do
resizeWindows()
redrawMenu()
elseif sEvent == "char" or sEvent == "key" or sEvent == "key_up" or sEvent == "paste" or sEvent == "terminate" then
-- Keyboard event
-- Passthrough to current process
elseif sEvent == "char" or sEvent == "key" or sEvent == "key_up" or sEvent == "paste" or sEvent == "terminate" or sEvent == "file_transfer" then
-- Basic input, just passthrough to current process
resumeProcess(nCurrentProcess, table.unpack(tEventData, 1, tEventData.n))
if cullProcess(nCurrentProcess) then
setMenuVisible(#tProcesses >= 2)

View File

@ -0,0 +1,24 @@
require "cc.completion"
print("Drop files to transfer them to this computer")
local files
while true do
local event, arg = os.pullEvent()
if event == "file_transfer" then
files = arg.getFiles()
break
elseif event == "key" and arg == keys.q then
return
end
end
if #files == 0 then
printError("No files to transfer")
return
end
package.path = package.path .. "/rom/modules/internal/?.lua"
local ok, err = require("cc.import")(files)
if not ok and err then printError(err) end

View File

@ -10,7 +10,6 @@
--
-- @module[module] shell
local expect = dofile("rom/modules/main/cc/expect.lua").expect
local make_package = dofile("rom/modules/main/cc/require.lua").make
local multishell = multishell
@ -35,6 +34,14 @@ local function createShellEnv(dir)
return env
end
-- Set up a dummy require based on the current shell, for loading some of our internal dependencies.
local require
do
local env = setmetatable(createShellEnv("/rom/modules/internal"), { __index = _ENV })
require = env.require
end
local expect = require("cc.expect").expect
-- Colours
local promptColour, textColour, bgColour
if term.isColour() then
@ -591,6 +598,13 @@ if #tArgs > 0 then
shell.run(...)
else
local function show_prompt()
term.setBackgroundColor(bgColour)
term.setTextColour(promptColour)
write(shell.dir() .. "> ")
term.setTextColour(textColour)
end
-- "shell"
-- Print the header
term.setBackgroundColor(bgColour)
@ -607,21 +621,49 @@ else
local tCommandHistory = {}
while not bExit do
term.redirect(parentTerm)
term.setBackgroundColor(bgColour)
term.setTextColour(promptColour)
write(shell.dir() .. "> ")
term.setTextColour(textColour)
show_prompt()
local sLine
if settings.get("shell.autocomplete") then
sLine = read(nil, tCommandHistory, shell.complete)
else
sLine = read(nil, tCommandHistory)
local complete
if settings.get("shell.autocomplete") then complete = shell.complete end
local ok, result
local co = coroutine.create(read)
assert(coroutine.resume(co, nil, tCommandHistory, complete))
while coroutine.status(co) ~= "dead" do
local event = table.pack(os.pullEvent())
if event[1] == "file_transfer" then
-- Abandon the current prompt
local _, h = term.getSize()
local _, y = term.getCursorPos()
if y == h then
term.scroll(1)
term.setCursorPos(1, y)
else
term.setCursorPos(1, y + 1)
end
term.setCursorBlink(false)
-- Run the import script with the provided files
local ok, err = require("cc.import")(event[2].getFiles())
if not ok and err then printError(err) end
-- And attempt to restore the prompt.
show_prompt()
term.setCursorBlink(true)
event = { "term_resize", n = 1 } -- Nasty hack to force read() to redraw.
end
if result == nil or event[1] == result or event[1] == "terminate" then
ok, result = coroutine.resume(co, table.unpack(event, 1, event.n))
if not ok then error(result, 0) end
end
end
if sLine:match("%S") and tCommandHistory[#tCommandHistory] ~= sLine then
table.insert(tCommandHistory, sLine)
if result:match("%S") and tCommandHistory[#tCommandHistory] ~= result then
table.insert(tCommandHistory, result)
end
shell.run(sLine)
shell.run(result)
end
end

View File

@ -11,18 +11,14 @@
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.core.computer.BasicEnvironment;
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.computer.FakeMainThreadScheduler;
import dan200.computercraft.core.filesystem.FileMount;
import dan200.computercraft.core.filesystem.FileSystemException;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.shared.peripheral.modem.ModemState;
import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemPeripheral;
import dan200.computercraft.support.TestFiles;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
import dan200.computercraft.test.core.computer.BasicEnvironment;
import dan200.computercraft.test.core.computer.FakeMainThreadScheduler;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.jupiter.api.*;
@ -32,8 +28,10 @@
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.net.URI;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.StandardCharsets;
@ -47,8 +45,6 @@
import java.util.regex.Pattern;
import java.util.stream.Stream;
import static dan200.computercraft.api.lua.LuaValues.getType;
/**
* Loads tests from {@code test-rom/spec} and executes them.
* <p>
@ -118,6 +114,7 @@ public void before() throws IOException
context = new ComputerContext( environment, 1, new FakeMainThreadScheduler() );
computer = new Computer( context, environment, term, 0 );
computer.getEnvironment().setPeripheral( ComputerSide.TOP, new FakeModem() );
computer.getEnvironment().setPeripheral( ComputerSide.BOTTOM, new FakePeripheralHub() );
computer.addApi( new CctTestAPI() );
computer.turnOn();
@ -198,36 +195,45 @@ public Stream<DynamicNode> get() throws InterruptedException
private static class DynamicNodeBuilder
{
private final String name;
private final URI uri;
private final Map<String, DynamicNodeBuilder> children;
private final Executable executor;
DynamicNodeBuilder( String name )
DynamicNodeBuilder( String name, String path )
{
this.name = name;
this.uri = getUri( path );
this.children = new HashMap<>();
this.executor = null;
}
DynamicNodeBuilder( String name, Executable executor )
DynamicNodeBuilder( String name, String path, Executable executor )
{
this.name = name;
this.uri = getUri( path );
this.children = Collections.emptyMap();
this.executor = executor;
}
private static URI getUri( String path )
{
// Unfortunately ?line=xxx doesn't appear to work with IntelliJ, so don't worry about getting it working.
return path == null ? null : new File( "src/test/resources" + path.substring( 0, path.indexOf( ':' ) ) ).toURI();
}
DynamicNodeBuilder get( String name )
{
DynamicNodeBuilder child = children.get( name );
if( child == null ) children.put( name, child = new DynamicNodeBuilder( name ) );
if( child == null ) children.put( name, child = new DynamicNodeBuilder( name, null ) );
return child;
}
void runs( String name, Executable executor )
void runs( String name, String uri, Executable executor )
{
if( this.executor != null ) throw new IllegalStateException( name + " is leaf node" );
if( children.containsKey( name ) ) throw new IllegalStateException( "Duplicate key for " + name );
children.put( name, new DynamicNodeBuilder( name, executor ) );
children.put( name, new DynamicNodeBuilder( name, uri, executor ) );
}
boolean isActive()
@ -244,8 +250,8 @@ boolean isActive()
DynamicNode build()
{
return executor == null
? DynamicContainer.dynamicContainer( name, buildChildren() )
: DynamicTest.dynamicTest( name, executor );
? DynamicContainer.dynamicContainer( name, uri, buildChildren() )
: DynamicTest.dynamicTest( name, uri, executor );
}
Stream<DynamicNode> buildChildren()
@ -282,26 +288,13 @@ private static String formatName( String name )
return name.replace( "\0", " -> " );
}
private static class FakeModem extends WirelessModemPeripheral
public static class FakeModem implements IPeripheral
{
FakeModem()
{
super( new ModemState(), true );
}
@Nonnull
@Override
@SuppressWarnings( "ConstantConditions" )
public Level getLevel()
public String getType()
{
return null;
}
@Nonnull
@Override
public Vec3 getPosition()
{
return Vec3.ZERO;
return "modem";
}
@Override
@ -309,6 +302,58 @@ public boolean equals( @Nullable IPeripheral other )
{
return this == other;
}
@LuaFunction
public final boolean isOpen( int channel )
{
return false;
}
}
public static class FakePeripheralHub implements IPeripheral
{
@Nonnull
@Override
public String getType()
{
return "peripheral_hub";
}
@Override
public boolean equals( @Nullable IPeripheral other )
{
return this == other;
}
@LuaFunction
public final Collection<String> getNamesRemote()
{
return Collections.singleton( "remote_1" );
}
@LuaFunction
public final boolean isPresentRemote( String name )
{
return name.equals( "remote_1" );
}
@LuaFunction
public final Object[] getTypeRemote( String name )
{
return name.equals( "remote_1" ) ? new Object[] { "remote", "other_type" } : null;
}
@LuaFunction
public final Object[] hasTypeRemote( String name, String type )
{
return name.equals( "remote_1" ) ? new Object[] { type.equals( "remote" ) || type.equals( "other_type" ) } : null;
}
@LuaFunction
public final Object[] getMethodsRemote( String name )
{
return name.equals( "remote_1" ) ? new Object[] { Collections.singletonList( "func" ) } : null;
}
}
public class CctTestAPI implements ILuaAPI
@ -340,15 +385,17 @@ public final void start( Map<?, ?> tests ) throws LuaException
{
// Submit several tests and signal for #get to run
LOG.info( "Received tests from computer" );
DynamicNodeBuilder root = new DynamicNodeBuilder( "" );
for( Object key : tests.keySet() )
DynamicNodeBuilder root = new DynamicNodeBuilder( "", null );
for( Map.Entry<?, ?> entry : tests.entrySet() )
{
if( !(key instanceof String name) ) throw new LuaException( "Non-key string " + getType( key ) );
String name = (String) entry.getKey();
Map<?, ?> details = (Map<?, ?>) entry.getValue();
String def = (String) details.get( "definition" );
String[] parts = name.split( "\0" );
DynamicNodeBuilder builder = root;
for( int i = 0; i < parts.length - 1; i++ ) builder = builder.get( parts[i] );
builder.runs( parts[parts.length - 1], () -> {
builder.runs( parts[parts.length - 1], def, () -> {
// Run it
lock.lockInterruptibly();
try

View File

@ -1,123 +0,0 @@
package dan200.computercraft.core.apis
import dan200.computercraft.ComputerCraft
import dan200.computercraft.api.lua.ILuaAPI
import dan200.computercraft.api.lua.MethodResult
import dan200.computercraft.api.peripheral.IPeripheral
import dan200.computercraft.api.peripheral.IWorkMonitor
import dan200.computercraft.core.computer.BasicEnvironment
import dan200.computercraft.core.computer.ComputerEnvironment
import dan200.computercraft.core.computer.ComputerSide
import dan200.computercraft.core.computer.GlobalEnvironment
import dan200.computercraft.core.filesystem.FileSystem
import dan200.computercraft.core.metrics.Metric
import dan200.computercraft.core.terminal.Terminal
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.ExperimentalTime
abstract class NullApiEnvironment : IAPIEnvironment {
private val computerEnv = BasicEnvironment()
override fun getComputerID(): Int = 0
override fun getComputerEnvironment(): ComputerEnvironment = computerEnv
override fun getGlobalEnvironment(): GlobalEnvironment = computerEnv
override fun getMainThreadMonitor(): IWorkMonitor = throw IllegalStateException("Work monitor not available")
override fun getTerminal(): Terminal = throw IllegalStateException("Terminal not available")
override fun getFileSystem(): FileSystem = throw IllegalStateException("Terminal not available")
override fun shutdown() {}
override fun reboot() {}
override fun setOutput(side: ComputerSide?, output: Int) {}
override fun getOutput(side: ComputerSide?): Int = 0
override fun getInput(side: ComputerSide?): Int = 0
override fun setBundledOutput(side: ComputerSide?, output: Int) {}
override fun getBundledOutput(side: ComputerSide?): Int = 0
override fun getBundledInput(side: ComputerSide?): Int = 0
override fun setPeripheralChangeListener(listener: IAPIEnvironment.IPeripheralChangeListener?) {}
override fun getPeripheral(side: ComputerSide?): IPeripheral? = null
override fun getLabel(): String? = null
override fun setLabel(label: String?) {}
override fun startTimer(ticks: Long): Int = 0
override fun cancelTimer(id: Int) {}
override fun observe(field: Metric.Counter) {}
override fun observe(field: Metric.Event, change: Long) {}
}
class EventResult(val name: String, val args: Array<Any?>)
class AsyncRunner : NullApiEnvironment() {
private val eventStream: Channel<Array<Any?>> = Channel(Int.MAX_VALUE)
private val apis: MutableList<ILuaAPI> = mutableListOf()
override fun queueEvent(event: String?, vararg args: Any?) {
ComputerCraft.log.debug("Queue event $event ${args.contentToString()}")
if (eventStream.trySend(arrayOf(event, *args)).isFailure) {
throw IllegalStateException("Queue is full")
}
}
override fun shutdown() {
super.shutdown()
eventStream.close()
apis.forEach { it.shutdown() }
}
fun <T : ILuaAPI> addApi(api: T): T {
apis.add(api)
api.startup()
return api
}
suspend fun resultOf(toRun: MethodResult): Array<Any?> {
var running = toRun
while (running.callback != null) running = runOnce(running)
return running.result ?: empty
}
private suspend fun runOnce(obj: MethodResult): MethodResult {
val callback = obj.callback ?: throw NullPointerException("Callback cannot be null")
val result = obj.result
val filter: String? = if (result.isNullOrEmpty() || result[0] !is String) {
null
} else {
result[0] as String
}
return callback.resume(pullEventImpl(filter))
}
private suspend fun pullEventImpl(filter: String?): Array<Any?> {
for (event in eventStream) {
ComputerCraft.log.debug("Pulled event ${event.contentToString()}")
val eventName = event[0] as String
if (filter == null || eventName == filter || eventName == "terminate") return event
}
throw IllegalStateException("No more events")
}
suspend fun pullEvent(filter: String? = null): EventResult {
val result = pullEventImpl(filter)
return EventResult(result[0] as String, result.copyOfRange(1, result.size))
}
companion object {
private val empty: Array<Any?> = arrayOf()
@OptIn(ExperimentalTime::class)
fun runTest(timeout: Duration = 5.seconds, fn: suspend AsyncRunner.() -> Unit) {
runBlocking {
val runner = AsyncRunner()
try {
withTimeout(timeout) { fn(runner) }
} finally {
runner.shutdown()
}
}
}
}
}

View File

@ -18,7 +18,7 @@
import java.util.Map;
import java.util.Optional;
import static dan200.computercraft.support.ContramapMatcher.contramap;
import static dan200.computercraft.test.core.ContramapMatcher.contramap;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertThrows;

View File

@ -13,8 +13,9 @@
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.ComputerContext;
import dan200.computercraft.core.computer.mainthread.MainThread;
import dan200.computercraft.core.filesystem.MemoryMount;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.test.core.computer.BasicEnvironment;
import dan200.computercraft.test.core.filesystem.MemoryMount;
import org.junit.jupiter.api.Assertions;
import java.util.Arrays;

View File

@ -8,6 +8,7 @@
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.core.lua.MachineResult;
import dan200.computercraft.support.ConcurrentHelpers;
import dan200.computercraft.test.core.computer.KotlinComputerManager;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -25,12 +26,12 @@
@Execution( ExecutionMode.CONCURRENT )
public class ComputerThreadTest
{
private FakeComputerManager manager;
private KotlinComputerManager manager;
@BeforeEach
public void before()
{
manager = new FakeComputerManager();
manager = new KotlinComputerManager();
}
@AfterEach

View File

@ -1,259 +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.core.computer;
import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.core.ComputerContext;
import dan200.computercraft.core.lua.ILuaMachine;
import dan200.computercraft.core.lua.MachineResult;
import dan200.computercraft.core.terminal.Terminal;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Creates "fake" computers, which just run user-defined tasks rather than Lua code.
*/
public class FakeComputerManager implements AutoCloseable
{
interface Task
{
MachineResult run( TimeoutState state ) throws Exception;
}
private final Map<Computer, Queue<Task>> machines = new HashMap<>();
private final ComputerContext context = new ComputerContext(
new BasicEnvironment(),
new ComputerThread( 1 ),
new FakeMainThreadScheduler(),
args -> new DummyLuaMachine( args.timeout() )
);
private final Lock errorLock = new ReentrantLock();
private final Condition hasError = errorLock.newCondition();
private volatile @Nullable Throwable error;
@Override
public void close()
{
try
{
context.ensureClosed( 1, TimeUnit.SECONDS );
}
catch( InterruptedException e )
{
throw new IllegalStateException( "Runtime thread was interrupted", e );
}
}
public ComputerContext context()
{
return context;
}
/**
* Create a new computer which pulls from our task queue.
*
* @return The computer. This will not be started yet, you must call {@link Computer#turnOn()} and
* {@link Computer#tick()} to do so.
*/
public Computer create()
{
Queue<Task> queue = new ConcurrentLinkedQueue<>();
Computer computer = new Computer( context, new BasicEnvironment(), new Terminal( 51, 19, true ), 0 );
computer.addApi( new QueuePassingAPI( queue ) ); // Inject an extra API to pass the queue to the machine.
machines.put( computer, queue );
return computer;
}
/**
* Create and start a new computer which loops forever.
*/
public void createLoopingComputer()
{
Computer computer = create();
enqueueForever( computer, t -> {
Thread.sleep( 100 );
return MachineResult.OK;
} );
computer.turnOn();
computer.tick();
}
/**
* Enqueue a task on a computer.
*
* @param computer The computer to enqueue the work on.
* @param task The task to run.
*/
public void enqueue( Computer computer, Task task )
{
machines.get( computer ).offer( task );
}
/**
* Enqueue a repeated task on a computer. This is automatically requeued when the task finishes, meaning the task
* queue is never empty.
*
* @param computer The computer to enqueue the work on.
* @param task The task to run.
*/
private void enqueueForever( Computer computer, Task task )
{
machines.get( computer ).offer( t -> {
MachineResult result = task.run( t );
enqueueForever( computer, task );
computer.queueEvent( "some_event", null );
return result;
} );
}
/**
* Sleep for a given period, immediately propagating any exceptions thrown by a computer.
*
* @param delay The duration to sleep for.
* @param unit The time unit the duration is measured in.
* @throws Exception An exception thrown by a running computer.
*/
public void sleep( long delay, TimeUnit unit ) throws Exception
{
errorLock.lock();
try
{
rethrowIfNeeded();
if( hasError.await( delay, unit ) ) rethrowIfNeeded();
}
finally
{
errorLock.unlock();
}
}
/**
* Start a computer and wait for it to finish.
*
* @param computer The computer to wait for.
* @throws Exception An exception thrown by a running computer.
*/
public void startAndWait( Computer computer ) throws Exception
{
computer.turnOn();
computer.tick();
do
{
sleep( 100, TimeUnit.MILLISECONDS );
} while( context.computerScheduler().hasPendingWork() || computer.isOn() );
rethrowIfNeeded();
}
private void rethrowIfNeeded() throws Exception
{
Throwable error = this.error;
if( error == null ) return;
if( error instanceof Exception ) throw (Exception) error;
rethrow( error );
}
@SuppressWarnings( "unchecked" )
private static <T extends Throwable> void rethrow( Throwable e ) throws T
{
throw (T) e;
}
private static final class QueuePassingAPI implements ILuaAPI
{
final Queue<Task> tasks;
private QueuePassingAPI( Queue<Task> tasks )
{
this.tasks = tasks;
}
@Override
public String[] getNames()
{
return new String[0];
}
}
private final class DummyLuaMachine implements ILuaMachine
{
private final TimeoutState state;
private @Nullable Queue<Task> tasks;
DummyLuaMachine( TimeoutState state )
{
this.state = state;
}
@Override
public void addAPI( @Nonnull ILuaAPI api )
{
if( api instanceof QueuePassingAPI ) tasks = ((QueuePassingAPI) api).tasks;
}
@Override
public MachineResult loadBios( @Nonnull InputStream bios )
{
return MachineResult.OK;
}
@Override
public MachineResult handleEvent( @Nullable String eventName, @Nullable Object[] arguments )
{
try
{
if( tasks == null ) throw new IllegalStateException( "Not received tasks yet" );
return tasks.remove().run( state );
}
catch( Throwable e )
{
errorLock.lock();
try
{
if( error == null )
{
error = e;
hasError.signal();
}
else
{
error.addSuppressed( e );
}
}
finally
{
errorLock.unlock();
}
if( !(e instanceof Exception) && !(e instanceof AssertionError) ) rethrow( e );
return MachineResult.error( e.getMessage() );
}
}
@Override
public void printExecutionState( StringBuilder out )
{
}
@Override
public void close()
{
}
}
}

View File

@ -7,7 +7,8 @@
import dan200.computercraft.api.lua.LuaValues;
import dan200.computercraft.shared.util.Colour;
import dan200.computercraft.support.CallCounter;
import dan200.computercraft.test.core.CallCounter;
import dan200.computercraft.test.core.terminal.TerminalMatchers;
import io.netty.buffer.Unpooled;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
@ -16,7 +17,7 @@
import java.nio.ByteBuffer;
import static dan200.computercraft.core.terminal.TerminalMatchers.*;
import static dan200.computercraft.test.core.terminal.TerminalMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.equalTo;

View File

@ -7,7 +7,7 @@
import dan200.computercraft.shared.computer.upload.FileSlice;
import dan200.computercraft.shared.computer.upload.FileUpload;
import dan200.computercraft.support.ArbitraryByteBuffer;
import dan200.computercraft.test.core.ArbitraryByteBuffer;
import dan200.computercraft.support.FakeContainer;
import dan200.computercraft.support.WithMinecraft;
import io.netty.buffer.Unpooled;
@ -22,9 +22,9 @@
import java.util.stream.Collectors;
import static dan200.computercraft.shared.network.server.UploadFileMessage.*;
import static dan200.computercraft.support.ByteBufferMatcher.bufferEqual;
import static dan200.computercraft.support.ContramapMatcher.contramap;
import static dan200.computercraft.support.CustomMatchers.containsWith;
import static dan200.computercraft.test.core.ByteBufferMatcher.bufferEqual;
import static dan200.computercraft.test.core.ContramapMatcher.contramap;
import static dan200.computercraft.test.core.CustomMatchers.containsWith;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertEquals;

View File

@ -1,8 +1,10 @@
package dan200.computercraft.core.apis.http.options
package dan200.computercraft.core.http
import dan200.computercraft.ComputerCraft
import dan200.computercraft.core.apis.AsyncRunner
import dan200.computercraft.core.apis.HTTPAPI
import dan200.computercraft.core.apis.http.options.Action
import dan200.computercraft.core.apis.http.options.AddressRule
import dan200.computercraft.test.core.computer.LuaTaskRunner
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertArrayEquals
import org.junit.jupiter.api.Assertions.assertEquals
@ -36,15 +38,15 @@ fun after() {
@Test
fun `Connects to websocket`() {
AsyncRunner.runTest {
val httpApi = addApi(HTTPAPI(this))
LuaTaskRunner.runTest {
val httpApi = addApi(HTTPAPI(environment))
val result = httpApi.websocket(WS_ADDRESS, Optional.empty())
assertArrayEquals(arrayOf(true), result, "Should have created websocket")
val event = pullEvent()
assertEquals("websocket_success", event.name) {
"Websocket failed to connect: ${event.args.contentToString()}"
assertEquals("websocket_success", event[0]) {
"Websocket failed to connect: ${event.contentToString()}"
}
}
}

View File

@ -424,6 +424,8 @@ local tests_locked = false
local test_list = {}
local test_map, test_count = {}, 0
local function format_loc(info) return ("%s:%d"):format(info.short_src, info.currentline) end
--- Add a new test to our queue.
--
-- @param test The descriptor of this test
@ -432,7 +434,7 @@ local function do_test(test)
if not test.name then test.name = table.concat(test_stack, "\0", 1, test_stack.n) end
test_count = test_count + 1
test_list[test_count] = test
test_map[test.name] = test_count
test_map[test.name] = { idx = test_count, definition = test.definition }
end
--- Get the "friendly" name of this test.
@ -456,7 +458,7 @@ local function describe(name, body)
local ok, err = try(body)
-- We count errors as a (failing) test.
if not ok then do_test { error = err } end
if not ok then do_test { error = err, definition = format_loc(debug.getinfo(2, "Sl")) } end
test_stack.n = n - 1
end
@ -475,7 +477,7 @@ local function it(name, body)
local n = test_stack.n + 1
test_stack[n], test_stack.n, tests_locked = name, n, true
do_test { action = body }
do_test { action = body, definition = format_loc(debug.getinfo(2, "Sl")) }
-- Pop the test from the stack
test_stack.n, tests_locked = n - 1, false
@ -488,12 +490,11 @@ local function pending(name)
check('it', 1, 'string', name)
if tests_locked then error("Cannot create test while running tests", 2) end
local _, loc = pcall(error, "", 3)
loc = loc:gsub(":%s*$", "")
local trace = format_loc(debug.getinfo(2, "Sl"))
local n = test_stack.n + 1
test_stack[n], test_stack.n = name, n
do_test { pending = true, trace = loc }
do_test { pending = true, trace = trace, definition = trace }
test_stack.n = n - 1
end
@ -667,7 +668,7 @@ if cct_test then
while true do
local _, name = os.pullEvent("cct_test_run")
if not name then break end
do_run(test_list[test_map[name]])
do_run(test_list[test_map[name].idx])
end
else
for _, test in pairs(test_list) do do_run(test) end

View File

@ -1,5 +1,6 @@
describe("The peripheral library", function()
local it_modem = peripheral.getType("top") == "modem" and it or pending
local it_remote = peripheral.getType("bottom") == "peripheral_hub" and it or pending
local multitype_peripheral = setmetatable({}, {
__name = "peripheral",
@ -13,6 +14,16 @@ describe("The peripheral library", function()
peripheral.isPresent("")
expect.error(peripheral.isPresent, nil):eq("bad argument #1 (expected string, got nil)")
end)
it_modem("asserts the presence of local peripherals", function()
expect(peripheral.isPresent("top")):eq(true)
expect(peripheral.isPresent("left")):eq(false)
end)
it_remote("asserts the presence of remote peripherals", function()
expect(peripheral.isPresent("remote_1")):eq(true)
expect(peripheral.isPresent("remote_2")):eq(false)
end)
end)
describe("peripheral.getName", function()
@ -24,6 +35,10 @@ describe("The peripheral library", function()
it_modem("can get the name of a wrapped peripheral", function()
expect(peripheral.getName(peripheral.wrap("top"))):eq("top")
end)
it("can get the name of a fake peripheral", function()
expect(peripheral.getName(multitype_peripheral)):eq("top")
end)
end)
describe("peripheral.getType", function()
@ -34,13 +49,18 @@ describe("The peripheral library", function()
end)
it("returns nil when no peripheral is present", function()
expect(peripheral.getType("bottom")):eq(nil)
expect(peripheral.getType("left")):eq(nil)
expect(peripheral.getType("remote_2")):eq(nil)
end)
it_modem("can get the type of a peripheral by side", function()
it_modem("can get the type of a local peripheral", function()
expect(peripheral.getType("top")):eq("modem")
end)
it_remote("can get the type of a remote peripheral", function()
expect(peripheral.getType("remote_1")):eq("remote")
end)
it_modem("can get the type of a wrapped peripheral", function()
expect(peripheral.getType(peripheral.wrap("top"))):eq("modem")
end)
@ -59,7 +79,8 @@ describe("The peripheral library", function()
end)
it("returns nil when no peripherals are present", function()
expect(peripheral.hasType("bottom", "modem")):eq(nil)
expect(peripheral.hasType("left", "modem")):eq(nil)
expect(peripheral.hasType("remote_2", "remote")):eq(nil)
end)
it_modem("can check type of a peripheral by side", function()
@ -76,6 +97,10 @@ describe("The peripheral library", function()
expect(peripheral.hasType(multitype_peripheral, "inventory")):eq(true)
expect(peripheral.hasType(multitype_peripheral, "something else")):eq(false)
end)
it_remote("can check type of a remote peripheral", function()
expect(peripheral.hasType("remote_1", "remote")):eq(true)
end)
end)
describe("peripheral.getMethods", function()
@ -103,6 +128,18 @@ describe("The peripheral library", function()
peripheral.wrap("")
expect.error(peripheral.wrap, nil):eq("bad argument #1 (expected string, got nil)")
end)
it_modem("wraps a local peripheral", function()
local p = peripheral.wrap("top")
expect(type(p)):eq("table")
expect(type(next(p))):eq("string")
end)
it_remote("wraps a remote peripheral", function()
local p = peripheral.wrap("remote_1")
expect(type(p)):eq("table")
expect(next(p)):eq("func")
end)
end)
describe("peripheral.find", function()
@ -113,5 +150,17 @@ describe("The peripheral library", function()
expect.error(peripheral.find, nil):eq("bad argument #1 (expected string, got nil)")
expect.error(peripheral.find, "", false):eq("bad argument #2 (expected function, got boolean)")
end)
it_modem("finds a local peripheral", function()
local p = peripheral.find("modem")
expect(type(p)):eq("table")
expect(peripheral.getName(p)):eq("top")
end)
it_modem("finds a local peripheral", function()
local p = peripheral.find("remote")
expect(type(p)):eq("table")
expect(peripheral.getName(p)):eq("remote_1")
end)
end)
end)

View File

@ -1,3 +1,5 @@
local with_window = require "test_helpers".with_window
describe("The shell", function()
describe("require", function()
it("validates arguments", function()
@ -101,4 +103,48 @@ describe("The shell", function()
expect.error(shell.switchTab, nil):eq("bad argument #1 (expected number, got nil)")
end)
end)
describe("file uploads", function()
local function create_file(name, contents)
local did_read = false
return {
getName = function() return name end,
read = function()
if did_read then return end
did_read = true
return contents
end,
close = function() end,
}
end
local function create_files(files) return { getFiles = function() return files end } end
it("suspends the read prompt", function()
fs.delete("tmp.txt")
local win = with_window(32, 5, function()
local queue = {
{ "shell" },
{ "paste", "xyz" },
{ "file_transfer", create_files { create_file("transfer.txt", "empty file") } },
}
local co = coroutine.create(shell.run)
for _, event in pairs(queue) do assert(coroutine.resume(co, table.unpack(event))) end
end)
expect(win.getCursorBlink()):eq(true)
local lines = {}
for i = 1, 5 do lines[i] = win.getLine(i):gsub(" +$", "") end
expect(lines):same {
"CraftOS 1.8",
"> xyz",
"Transferring transfer.txt",
"> xyz",
"",
}
expect({ win.getCursorPos() }):same { 6, 4 }
end)
end)
end)

View File

@ -3,7 +3,7 @@
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.support;
package dan200.computercraft.test.core;
import net.jqwik.api.*;
import net.jqwik.api.arbitraries.SizableArbitrary;

View File

@ -3,7 +3,7 @@
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.support;
package dan200.computercraft.test.core;
import org.hamcrest.Description;
import org.hamcrest.Matcher;

View File

@ -3,7 +3,7 @@
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.support;
package dan200.computercraft.test.core;
import static org.junit.jupiter.api.Assertions.assertEquals;

View File

@ -3,7 +3,7 @@
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.support;
package dan200.computercraft.test.core;
import org.hamcrest.FeatureMatcher;
import org.hamcrest.Matcher;

View File

@ -3,7 +3,7 @@
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.support;
package dan200.computercraft.test.core;
import org.hamcrest.Matcher;

View File

@ -0,0 +1,161 @@
/*
* 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.test.core.apis;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.peripheral.IWorkMonitor;
import dan200.computercraft.core.apis.IAPIEnvironment;
import dan200.computercraft.core.computer.ComputerEnvironment;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.computer.GlobalEnvironment;
import dan200.computercraft.core.filesystem.FileSystem;
import dan200.computercraft.core.metrics.Metric;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.test.core.computer.BasicEnvironment;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public abstract class BasicApiEnvironment implements IAPIEnvironment
{
private final BasicEnvironment environment;
private @Nullable String label;
public BasicApiEnvironment( BasicEnvironment environment )
{
this.environment = environment;
}
@Override
public int getComputerID()
{
return 0;
}
@Nonnull
@Override
public ComputerEnvironment getComputerEnvironment()
{
return environment;
}
@Nonnull
@Override
public GlobalEnvironment getGlobalEnvironment()
{
return environment;
}
@Nonnull
@Override
public IWorkMonitor getMainThreadMonitor()
{
throw new IllegalStateException( "Main thread monitor not available" );
}
@Nonnull
@Override
public Terminal getTerminal()
{
throw new IllegalStateException( "Terminal not available" );
}
@Override
public FileSystem getFileSystem()
{
throw new IllegalStateException( "Filesystem not available" );
}
@Override
public void shutdown()
{
}
@Override
public void reboot()
{
}
@Override
public void setOutput( ComputerSide side, int output )
{
}
@Override
public int getOutput( ComputerSide side )
{
return 0;
}
@Override
public int getInput( ComputerSide side )
{
return 0;
}
@Override
public void setBundledOutput( ComputerSide side, int output )
{
}
@Override
public int getBundledOutput( ComputerSide side )
{
return 0;
}
@Override
public int getBundledInput( ComputerSide side )
{
return 0;
}
@Override
public void setPeripheralChangeListener( @Nullable IPeripheralChangeListener listener )
{
}
@Nullable
@Override
public IPeripheral getPeripheral( ComputerSide side )
{
return null;
}
@Nullable
@Override
public String getLabel()
{
return label;
}
@Override
public void setLabel( @Nullable String label )
{
this.label = label;
}
@Override
public int startTimer( long ticks )
{
throw new IllegalStateException( "Cannot start timers" );
}
@Override
public void cancelTimer( int id )
{
}
@Override
public void observe( @Nonnull Metric.Event summary, long value )
{
}
@Override
public void observe( @Nonnull Metric.Counter counter )
{
}
}

View File

@ -3,16 +3,18 @@
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.computer;
package dan200.computercraft.test.core.computer;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.filesystem.IMount;
import dan200.computercraft.api.filesystem.IWritableMount;
import dan200.computercraft.core.computer.ComputerEnvironment;
import dan200.computercraft.core.computer.GlobalEnvironment;
import dan200.computercraft.core.filesystem.FileMount;
import dan200.computercraft.core.filesystem.JarMount;
import dan200.computercraft.core.filesystem.MemoryMount;
import dan200.computercraft.core.metrics.Metric;
import dan200.computercraft.core.metrics.MetricsObserver;
import dan200.computercraft.test.core.filesystem.MemoryMount;
import javax.annotation.Nonnull;
import java.io.File;
@ -24,7 +26,8 @@
import java.net.URL;
/**
* A very basic environment.
* A basic implementation of {@link ComputerEnvironment} and {@link GlobalEnvironment}, suitable for a context which
* will only run a single computer.
*/
public class BasicEnvironment implements ComputerEnvironment, GlobalEnvironment, MetricsObserver
{

View File

@ -3,7 +3,7 @@
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.computer;
package dan200.computercraft.test.core.computer;
import dan200.computercraft.core.computer.mainthread.MainThreadScheduler;
import dan200.computercraft.core.metrics.MetricsObserver;

View File

@ -3,7 +3,7 @@
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.filesystem;
package dan200.computercraft.test.core.filesystem;
import dan200.computercraft.api.filesystem.IWritableMount;
import dan200.computercraft.core.apis.handles.ArrayByteChannel;

View File

@ -3,9 +3,11 @@
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.terminal;
package dan200.computercraft.test.core.terminal;
import dan200.computercraft.support.ContramapMatcher;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.terminal.TextBuffer;
import dan200.computercraft.test.core.ContramapMatcher;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;

View File

@ -0,0 +1,63 @@
/*
* 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.test.core
import org.hamcrest.BaseMatcher
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.collection.IsArray
import org.junit.jupiter.api.Assertions
/** Postfix version of [Assertions.assertArrayEquals] */
fun Array<out Any?>?.assertArrayEquals(vararg expected: Any?, message: String? = null) {
assertThat(
message ?: "",
this,
IsArrayVerbose(expected.map { FuzzyEqualTo(it) }.toTypedArray()),
)
}
/**
* Extension of [IsArray] which always prints the array, not just when the items are mismatched.
*/
internal class IsArrayVerbose<T>(private val elementMatchers: Array<Matcher<in T>>) : IsArray<T>(elementMatchers) {
override fun describeMismatchSafely(actual: Array<out T>, description: Description) {
description.appendText("array was ").appendValue(actual)
if (actual.size != elementMatchers.size) {
description.appendText(" with length ").appendValue(actual.size)
return
}
for (i in actual.indices) {
if (!elementMatchers[i].matches(actual[i])) {
description.appendText("with element ").appendValue(i).appendText(" ")
elementMatchers[i].describeMismatch(actual[i], description)
return
}
}
}
}
/**
* An equality matcher which is slightly more relaxed on comparing some values.
*/
internal class FuzzyEqualTo(private val expected: Any?) : BaseMatcher<Any?>() {
override fun describeTo(description: Description) {
description.appendValue(expected)
}
override fun matches(actual: Any?): Boolean {
if (actual == null) return false
if (actual is Number && expected is Number && actual.javaClass != expected.javaClass) {
// Allow equating integers and floats.
return actual.toDouble() == expected.toDouble()
}
return actual == expected
}
}

View File

@ -0,0 +1,188 @@
/*
* 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.test.core.computer
import dan200.computercraft.api.lua.ILuaAPI
import dan200.computercraft.core.ComputerContext
import dan200.computercraft.core.computer.Computer
import dan200.computercraft.core.computer.ComputerThread
import dan200.computercraft.core.computer.TimeoutState
import dan200.computercraft.core.lua.MachineEnvironment
import dan200.computercraft.core.lua.MachineResult
import dan200.computercraft.core.terminal.Terminal
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock
typealias FakeComputerTask = (state: TimeoutState) -> MachineResult
/**
* Creates "fake" computers, which just run user-defined tasks rather than Lua code.
*/
class KotlinComputerManager : AutoCloseable {
private val machines: MutableMap<Computer, Queue<FakeComputerTask>> = HashMap()
private val context = ComputerContext(BasicEnvironment(), ComputerThread(1), FakeMainThreadScheduler()) { DummyLuaMachine(it) }
private val errorLock: Lock = ReentrantLock()
private val hasError = errorLock.newCondition()
@Volatile
private var error: Throwable? = null
override fun close() {
try {
context.ensureClosed(1, TimeUnit.SECONDS)
} catch (e: InterruptedException) {
throw IllegalStateException("Runtime thread was interrupted", e)
}
}
fun context(): ComputerContext {
return context
}
/**
* Create a new computer which pulls from our task queue.
*
* @return The computer. This will not be started yet, you must call [Computer.turnOn] and
* [Computer.tick] to do so.
*/
fun create(): Computer {
val queue: Queue<FakeComputerTask> = ConcurrentLinkedQueue()
val computer = Computer(context, BasicEnvironment(), Terminal(51, 19, true), 0)
computer.addApi(QueuePassingAPI(queue)) // Inject an extra API to pass the queue to the machine.
machines[computer] = queue
return computer
}
/**
* Create and start a new computer which loops forever.
*/
fun createLoopingComputer() {
val computer = create()
enqueueForever(computer) {
Thread.sleep(100)
MachineResult.OK
}
computer.turnOn()
computer.tick()
}
/**
* Enqueue a task on a computer.
*
* @param computer The computer to enqueue the work on.
* @param task The task to run.
*/
fun enqueue(computer: Computer, task: FakeComputerTask) {
machines[computer]!!.offer(task)
}
/**
* Enqueue a repeated task on a computer. This is automatically requeued when the task finishes, meaning the task
* queue is never empty.
*
* @param computer The computer to enqueue the work on.
* @param task The task to run.
*/
private fun enqueueForever(computer: Computer, task: FakeComputerTask) {
machines[computer]!!.offer {
val result = task(it)
enqueueForever(computer, task)
computer.queueEvent("some_event", null)
result
}
}
/**
* Sleep for a given period, immediately propagating any exceptions thrown by a computer.
*
* @param delay The duration to sleep for.
* @param unit The time unit the duration is measured in.
* @throws Exception An exception thrown by a running computer.
*/
@Throws(Exception::class)
fun sleep(delay: Long, unit: TimeUnit?) {
errorLock.lock()
try {
rethrowIfNeeded()
if (hasError.await(delay, unit)) rethrowIfNeeded()
} finally {
errorLock.unlock()
}
}
/**
* Start a computer and wait for it to finish.
*
* @param computer The computer to wait for.
* @throws Exception An exception thrown by a running computer.
*/
@Throws(Exception::class)
fun startAndWait(computer: Computer) {
computer.turnOn()
computer.tick()
do {
sleep(100, TimeUnit.MILLISECONDS)
} while (context.computerScheduler().hasPendingWork() || computer.isOn)
rethrowIfNeeded()
}
@Throws(Exception::class)
private fun rethrowIfNeeded() {
val error = error ?: return
throw error
}
private class QueuePassingAPI constructor(val tasks: Queue<FakeComputerTask>) : ILuaAPI {
override fun getNames(): Array<String> = arrayOf()
}
private inner class DummyLuaMachine(private val environment: MachineEnvironment) : KotlinLuaMachine(environment) {
private var tasks: Queue<FakeComputerTask>? = null
override fun addAPI(api: ILuaAPI) {
super.addAPI(api)
if (api is QueuePassingAPI) tasks = api.tasks
}
override fun getTask(): (suspend KotlinLuaMachine.() -> Unit)? {
try {
val tasks = this.tasks ?: throw NullPointerException("Not received tasks yet")
val task = tasks.remove()
return {
try {
task(environment.timeout)
} catch (e: Throwable) {
reportError(e)
}
}
} catch (e: Throwable) {
reportError(e)
return null
}
}
override fun close() {}
private fun reportError(e: Throwable) {
errorLock.lock()
try {
if (error == null) {
error = e
hasError.signal()
} else {
error!!.addSuppressed(e)
}
} finally {
errorLock.unlock()
}
if (e is Exception || e is AssertionError) return else throw e
}
}
}

View File

@ -0,0 +1,41 @@
package dan200.computercraft.test.core.computer
import dan200.computercraft.api.lua.ILuaAPI
import dan200.computercraft.api.lua.ILuaContext
import dan200.computercraft.core.lua.ILuaMachine
import dan200.computercraft.core.lua.MachineEnvironment
import dan200.computercraft.core.lua.MachineResult
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.InputStream
/**
* An [ILuaMachine] which runs Kotlin functions instead.
*/
abstract class KotlinLuaMachine(environment: MachineEnvironment) : ILuaMachine, AbstractLuaTaskContext() {
override val context: ILuaContext = environment.context
override fun addAPI(api: ILuaAPI) = addApi(api)
override fun loadBios(bios: InputStream): MachineResult = MachineResult.OK
override fun handleEvent(eventName: String?, arguments: Array<out Any>?): MachineResult {
if (hasEventListeners) {
queueEvent(eventName, arguments)
} else {
val task = getTask()
if (task != null) CoroutineScope(Dispatchers.Unconfined + CoroutineName("Computer")).launch { task() }
}
return MachineResult.OK
}
override fun printExecutionState(out: StringBuilder) {}
/**
* Get the next task to execute on this computer.
*/
protected abstract fun getTask(): (suspend KotlinLuaMachine.() -> Unit)?
}

View File

@ -0,0 +1,87 @@
package dan200.computercraft.test.core.computer
import dan200.computercraft.api.lua.ILuaAPI
import dan200.computercraft.api.lua.ILuaContext
import dan200.computercraft.api.lua.MethodResult
import dan200.computercraft.api.lua.ObjectArguments
import dan200.computercraft.core.apis.PeripheralAPI
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.suspendCancellableCoroutine
/**
* The context for tasks which consume Lua objects.
*
* This provides helpers for converting CC's callback-based code into a more direct style based on Kotlin coroutines.
*/
interface LuaTaskContext {
/** The current Lua context, to be passed to method calls. */
val context: ILuaContext
/** Get a registered API. */
fun <T : ILuaAPI> getApi(api: Class<T>): T
/** Pull a Lua event */
suspend fun pullEvent(event: String? = null): Array<out Any?>
/** Resolve a [MethodResult] until completion, returning the resulting values. */
suspend fun MethodResult.await(): Array<out Any?>? {
var result = this
while (true) {
val callback = result.callback
val values = result.result
if (callback == null) return values
val filter = if (values == null) null else values[0] as String?
result = callback.resume(pullEvent(filter))
}
}
/** Call a peripheral method. */
suspend fun LuaTaskContext.callPeripheral(name: String, method: String, vararg args: Any?): Array<out Any?>? =
getApi<PeripheralAPI>().call(context, ObjectArguments(name, method, *args)).await()
}
/** Get a registered API. */
inline fun <reified T : ILuaAPI> LuaTaskContext.getApi(): T = getApi(T::class.java)
abstract class AbstractLuaTaskContext : LuaTaskContext, AutoCloseable {
private val pullEvents = mutableListOf<PullEvent>()
private val apis = mutableMapOf<Class<out ILuaAPI>, ILuaAPI>()
protected fun addApi(api: ILuaAPI) {
apis[api.javaClass] = api
}
protected val hasEventListeners
get() = pullEvents.isNotEmpty()
protected fun queueEvent(eventName: String?, arguments: Array<out Any?>?) {
val fullEvent: Array<out Any?> = when {
eventName == null && arguments == null -> arrayOf()
eventName != null && arguments == null -> arrayOf(eventName)
eventName == null && arguments != null -> arguments
else -> arrayOf(eventName, *arguments!!)
}
for (i in pullEvents.size - 1 downTo 0) {
val puller = pullEvents[i]
if (puller.name == null || puller.name == eventName || eventName == "terminate") {
pullEvents.removeAt(i)
puller.cont.resumeWith(Result.success(fullEvent))
}
}
}
override fun close() {
for (pullEvent in pullEvents) pullEvent.cont.cancel()
pullEvents.clear()
}
final override fun <T : ILuaAPI> getApi(api: Class<T>): T =
api.cast(apis[api] ?: throw IllegalStateException("No API of type ${api.name}"))
final override suspend fun pullEvent(event: String?): Array<out Any?> =
suspendCancellableCoroutine { cont -> pullEvents.add(PullEvent(event, cont)) }
private class PullEvent(val name: String?, val cont: CancellableContinuation<Array<out Any?>>)
}

View File

@ -0,0 +1,64 @@
package dan200.computercraft.test.core.computer
import dan200.computercraft.api.lua.ILuaAPI
import dan200.computercraft.api.lua.ILuaContext
import dan200.computercraft.api.lua.LuaException
import dan200.computercraft.core.apis.IAPIEnvironment
import dan200.computercraft.test.core.apis.BasicApiEnvironment
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
class LuaTaskRunner : AbstractLuaTaskContext() {
private val eventStream: Channel<Event> = Channel(Channel.UNLIMITED)
private val apis = mutableListOf<ILuaAPI>()
val environment: IAPIEnvironment = object : BasicApiEnvironment(BasicEnvironment()) {
override fun queueEvent(event: String?, vararg args: Any?) {
if (eventStream.trySend(Event(event, args)).isFailure) {
throw IllegalStateException("Queue is full")
}
}
override fun shutdown() {
super.shutdown()
eventStream.close()
}
}
override val context =
ILuaContext { throw LuaException("Cannot queue main thread task") }
fun <T : ILuaAPI> addApi(api: T): T {
super.addApi(api)
apis.add(api)
api.startup()
return api
}
override fun close() {
environment.shutdown()
}
private suspend fun run() {
for (event in eventStream) {
queueEvent(event.name, event.args)
}
}
private class Event(val name: String?, val args: Array<out Any?>)
companion object {
fun runTest(timeout: Duration = 5.seconds, fn: suspend LuaTaskRunner.() -> Unit) {
runBlocking {
withTimeout(timeout) {
val runner = LuaTaskRunner()
launch { runner.run() }
runner.use { fn(runner) }
}
}
}
}
}