mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2024-12-14 04:00:30 +00:00
Merge branch 'mc-1.18.x' into mc-1.19.x
This commit is contained in:
commit
b5056fc3b8
@ -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 {
|
||||
@ -145,15 +147,13 @@ dependencies {
|
||||
"shade"(libs.cobalt)
|
||||
"shade"("io.netty:netty-codec-http:4.1.76.Final")
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@ -191,7 +191,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)
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -221,7 +221,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",
|
||||
)
|
||||
@ -314,7 +314,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) {
|
||||
|
@ -9,6 +9,7 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.kotlin.plugin)
|
||||
implementation(libs.spotless)
|
||||
}
|
||||
|
||||
|
53
buildSrc/src/main/kotlin/cc-tweaked.gametest.gradle.kts
Normal file
53
buildSrc/src/main/kotlin/cc-tweaked.gametest.gradle.kts
Normal 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))
|
||||
}
|
@ -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()
|
||||
|
@ -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()}"
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package cc.tweaked.gradle
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
42
doc/events/file_transfer.md
Normal file
42
doc/events/file_transfer.md
Normal 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
|
||||
```
|
@ -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]
|
||||
|
@ -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()
|
||||
|
@ -17,29 +17,33 @@ import dan200.computercraft.shared.computer.inventory.ContainerComputerBase;
|
||||
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.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 = Component.translatable( "gui.ok" );
|
||||
private static final Component CANCEL = Component.translatable( "gui.cancel" );
|
||||
private static final Component OVERWRITE = Component.translatable( "gui.computercraft.upload.overwrite_button" );
|
||||
private static final Component NO_RESPONSE_TITLE = Component.translatable( "gui.computercraft.upload.no_response" );
|
||||
private static final Component NO_RESPONSE_MSG = Component.translatable( "gui.computercraft.upload.no_response.msg",
|
||||
Component.literal( "import" ).withStyle( ChatFormatting.DARK_GRAY ) );
|
||||
|
||||
protected WidgetTerminal terminal;
|
||||
protected Terminal terminalData;
|
||||
@ -48,11 +52,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;
|
||||
}
|
||||
@ -82,6 +90,13 @@ public abstract class ComputerScreenBase<T extends ContainerComputerBase> extend
|
||||
{
|
||||
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
|
||||
@ -193,41 +208,29 @@ public abstract class ComputerScreenBase<T extends ContainerComputerBase> extend
|
||||
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,
|
||||
|
150
src/main/java/dan200/computercraft/client/gui/ItemToast.java
Normal file
150
src/main/java/dan200/computercraft/client/gui/ItemToast.java
Normal 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 );
|
||||
}
|
||||
}
|
@ -485,7 +485,9 @@ public class OSAPI implements ILuaAPI
|
||||
|
||||
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 );
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 @@ public final class ComputerThread
|
||||
*
|
||||
* @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();
|
||||
|
@ -292,6 +292,7 @@ public final class ResourceMount implements IMount
|
||||
try
|
||||
{
|
||||
for( ResourceMount mount : MOUNT_CACHE.values() ) mount.load( manager );
|
||||
CONTENTS_CACHE.invalidateAll();
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
@ -12,7 +12,6 @@ import dan200.computercraft.core.apis.http.options.Action;
|
||||
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;
|
||||
@ -79,6 +78,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;
|
||||
@ -87,7 +87,7 @@ public final class Config
|
||||
|
||||
static
|
||||
{
|
||||
Builder builder = new Builder();
|
||||
ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder();
|
||||
|
||||
{ // General computers
|
||||
computerSpaceLimit = builder
|
||||
@ -271,13 +271,17 @@ public final class 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();
|
||||
}
|
||||
|
||||
@ -345,6 +349,7 @@ public final class Config
|
||||
{
|
||||
ComputerCraft.monitorRenderer = monitorRenderer.get();
|
||||
ComputerCraft.monitorDistance = monitorDistance.get();
|
||||
ComputerCraft.uploadNagDelay = uploadNagDelay.get();
|
||||
}
|
||||
|
||||
private static void sync( ModConfig config )
|
||||
|
@ -33,6 +33,7 @@ import net.minecraft.world.entity.Entity;
|
||||
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;
|
||||
@ -218,7 +219,7 @@ public final class CommandComputerCraft
|
||||
.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
|
||||
|
@ -19,17 +19,9 @@ import net.minecraft.world.level.block.state.BlockState;
|
||||
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 );
|
||||
|
@ -139,7 +139,11 @@ public abstract class TileComputerBase extends TileGeneric implements IComputerT
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import dan200.computercraft.core.terminal.Terminal;
|
||||
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.AbstractContainerMenu;
|
||||
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 abstract class ContainerComputerBase extends AbstractContainerMenu implem
|
||||
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 abstract class ContainerComputerBase extends AbstractContainerMenu implem
|
||||
}
|
||||
|
||||
@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 abstract class ContainerComputerBase extends AbstractContainerMenu implem
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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 );
|
||||
}
|
||||
|
@ -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.IntOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.ints.IntSet;
|
||||
import net.minecraft.network.chat.Component;
|
||||
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 class ServerInputState implements ServerInputHandler
|
||||
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, Component.translatable( "gui.computercraft.upload.failed.corrupted" ) );
|
||||
return UploadResultMessage.error( owner, Component.translatable( "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,
|
||||
Component.translatable( "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,
|
||||
Component.translatable( "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, Component.translatable( "gui.computercraft.upload.success.msg", files.size() )
|
||||
);
|
||||
}
|
||||
catch( FileSystemException | IOException e )
|
||||
{
|
||||
ComputerCraft.log.error( "Error uploading files", e );
|
||||
return new UploadResultMessage( UploadResult.ERROR, Component.translatable( "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()
|
||||
|
@ -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 );
|
||||
}
|
||||
}
|
@ -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 ) );
|
||||
}
|
||||
}
|
||||
}
|
@ -9,16 +9,13 @@ import net.minecraft.network.chat.Component;
|
||||
|
||||
public enum UploadResult
|
||||
{
|
||||
SUCCESS,
|
||||
ERROR,
|
||||
CONFIRM_OVERWRITE;
|
||||
QUEUED,
|
||||
CONSUMED,
|
||||
ERROR;
|
||||
|
||||
public static final Component SUCCESS_TITLE = Component.translatable( "gui.computercraft.upload.success" );
|
||||
|
||||
public static final Component FAILED_TITLE = Component.translatable( "gui.computercraft.upload.failed" );
|
||||
public static final Component COMPUTER_OFF_MSG = Component.translatable( "gui.computercraft.upload.failed.computer_off" );
|
||||
public static final Component OUT_OF_SPACE_MSG = Component.translatable( "gui.computercraft.upload.failed.out_of_space" );
|
||||
public static final Component TOO_MUCH_MSG = Component.translatable( "gui.computercraft.upload.failed.too_much" );
|
||||
|
||||
public static final Component UPLOAD_OVERWRITE = Component.translatable( "gui.computercraft.upload.overwrite" );
|
||||
}
|
||||
|
@ -50,7 +50,6 @@ public final class NetworkHandler
|
||||
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 );
|
||||
|
@ -13,35 +13,53 @@ import net.minecraft.client.Minecraft;
|
||||
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 class UploadResultMessage implements NetworkMessage
|
||||
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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,22 +9,28 @@ import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
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 class ComputerContainerData implements ContainerData
|
||||
{
|
||||
buf.writeEnum( family );
|
||||
terminal.write( buf );
|
||||
buf.writeItemStack( displayStack, true );
|
||||
}
|
||||
|
||||
public ComputerFamily family()
|
||||
@ -43,4 +50,15 @@ public class ComputerContainerData implements ContainerData
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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 );
|
||||
}
|
||||
}
|
@ -87,8 +87,9 @@ public class TileCable extends TileGeneric
|
||||
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 class TileCable extends TileGeneric
|
||||
public void clearRemoved()
|
||||
{
|
||||
super.clearRemoved(); // TODO: Replace with onLoad
|
||||
TickScheduler.schedule( this );
|
||||
TickScheduler.schedule( tickToken );
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -241,7 +242,7 @@ public class TileCable extends TileGeneric
|
||||
{
|
||||
if( invalidPeripheral ) return;
|
||||
invalidPeripheral = true;
|
||||
TickScheduler.schedule( this );
|
||||
TickScheduler.schedule( tickToken );
|
||||
}
|
||||
|
||||
private void refreshPeripheral()
|
||||
|
@ -100,7 +100,8 @@ public class TileWiredModemFull extends TileGeneric
|
||||
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 class TileWiredModemFull extends TileGeneric
|
||||
|
||||
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 @@ public class TileWiredModemFull extends TileGeneric
|
||||
public void clearRemoved()
|
||||
{
|
||||
super.clearRemoved(); // TODO: Replace with onLoad
|
||||
TickScheduler.schedule( this );
|
||||
TickScheduler.schedule( tickToken );
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -72,6 +72,13 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements IW
|
||||
protected abstract WiredModemLocalPeripheral getLocalPeripheral();
|
||||
//endregion
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<String> getAdditionalTypes()
|
||||
{
|
||||
return Collections.singleton( "peripheral_hub" );
|
||||
}
|
||||
|
||||
//region Peripheral methods
|
||||
|
||||
/**
|
||||
|
@ -33,7 +33,7 @@ public class TileWirelessModem extends TileGeneric
|
||||
|
||||
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 class TileWirelessModem extends TileGeneric
|
||||
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 class TileWirelessModem extends TileGeneric
|
||||
public void clearRemoved()
|
||||
{
|
||||
super.clearRemoved(); // TODO: Replace with onLoad
|
||||
TickScheduler.schedule( this );
|
||||
TickScheduler.schedule( tickToken );
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -64,7 +64,7 @@ public class ServerMonitor
|
||||
|
||||
private void markChanged()
|
||||
{
|
||||
if( !changed.getAndSet( true ) ) TickScheduler.schedule( origin );
|
||||
if( !changed.getAndSet( true ) ) TickScheduler.schedule( origin.tickToken );
|
||||
}
|
||||
|
||||
int getTextScale()
|
||||
|
@ -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 class TileMonitor extends TileGeneric
|
||||
{
|
||||
super.clearRemoved();
|
||||
needsValidating = true; // Same, tbh
|
||||
TickScheduler.schedule( this );
|
||||
TickScheduler.schedule( tickToken );
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -163,7 +163,7 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I
|
||||
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 );
|
||||
|
@ -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 @@ public final class 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 final class TickScheduler
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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")
|
||||
|
@ -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)
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -11,18 +11,14 @@ import dan200.computercraft.api.lua.ILuaAPI;
|
||||
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 org.opentest4j.AssertionFailedError;
|
||||
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.Matcher;
|
||||
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 class ComputerTestDelegate
|
||||
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 class ComputerTestDelegate
|
||||
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 @@ public class ComputerTestDelegate
|
||||
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 @@ public class ComputerTestDelegate
|
||||
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 class ComputerTestDelegate
|
||||
{
|
||||
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 class ComputerTestDelegate
|
||||
{
|
||||
// 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
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@ import java.util.List;
|
||||
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;
|
||||
|
@ -13,8 +13,9 @@ import dan200.computercraft.api.lua.LuaException;
|
||||
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;
|
||||
|
@ -8,6 +8,7 @@ package dan200.computercraft.core.computer;
|
||||
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 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
@Execution( ExecutionMode.CONCURRENT )
|
||||
public class ComputerThreadTest
|
||||
{
|
||||
private FakeComputerManager manager;
|
||||
private KotlinComputerManager manager;
|
||||
|
||||
@BeforeEach
|
||||
public void before()
|
||||
{
|
||||
manager = new FakeComputerManager();
|
||||
manager = new KotlinComputerManager();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -7,7 +7,8 @@ package dan200.computercraft.core.terminal;
|
||||
|
||||
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 org.junit.jupiter.api.Test;
|
||||
|
||||
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;
|
||||
|
@ -7,7 +7,7 @@ package dan200.computercraft.shared.network.server;
|
||||
|
||||
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.List;
|
||||
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;
|
||||
|
@ -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 @@ class TestHttpApi {
|
||||
|
||||
@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()}"
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
|
@ -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;
|
@ -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;
|
||||
|
@ -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 )
|
||||
{
|
||||
}
|
||||
}
|
@ -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.URISyntaxException;
|
||||
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
|
||||
{
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)?
|
||||
}
|
@ -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?>>)
|
||||
}
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user