mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-07-16 17:02:52 +00:00
Merge branch 'mc-1.16.x' into mc-1.18.x
This commit is contained in:
commit
38b2c944f3
@ -1,9 +1,9 @@
|
|||||||
import cc.tweaked.gradle.*
|
import cc.tweaked.gradle.*
|
||||||
import net.darkhax.curseforgegradle.TaskPublishCurseForge
|
import net.darkhax.curseforgegradle.TaskPublishCurseForge
|
||||||
|
import net.minecraftforge.gradle.common.util.RunConfig
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
// Build
|
// Build
|
||||||
alias(libs.plugins.kotlin)
|
|
||||||
alias(libs.plugins.forgeGradle)
|
alias(libs.plugins.forgeGradle)
|
||||||
alias(libs.plugins.mixinGradle)
|
alias(libs.plugins.mixinGradle)
|
||||||
alias(libs.plugins.librarian)
|
alias(libs.plugins.librarian)
|
||||||
@ -18,7 +18,7 @@ plugins {
|
|||||||
|
|
||||||
id("cc-tweaked.illuaminate")
|
id("cc-tweaked.illuaminate")
|
||||||
id("cc-tweaked.node")
|
id("cc-tweaked.node")
|
||||||
id("cc-tweaked.java-convention")
|
id("cc-tweaked.gametest")
|
||||||
id("cc-tweaked")
|
id("cc-tweaked")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,8 +36,6 @@ sourceSets {
|
|||||||
main {
|
main {
|
||||||
resources.srcDir("src/generated/resources")
|
resources.srcDir("src/generated/resources")
|
||||||
}
|
}
|
||||||
|
|
||||||
register("testMod")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
minecraft {
|
minecraft {
|
||||||
@ -80,29 +78,36 @@ minecraft {
|
|||||||
property("cct.pretty-json", "true")
|
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 {
|
val testClient by registering {
|
||||||
workingDirectory(file("run/testClient"))
|
workingDirectory(file("run/testClient"))
|
||||||
parent(client.get())
|
parent(client.get())
|
||||||
|
configureForGameTest()
|
||||||
mods.register("cctest") { source(sourceSets["testMod"]) }
|
|
||||||
|
|
||||||
lazyToken("minecraft_classpath") {
|
|
||||||
(configurations["shade"].copyRecursive().resolve() + configurations["testModExtra"].copyRecursive().resolve())
|
|
||||||
.joinToString(File.pathSeparator) { it.absolutePath }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val gameTestServer by registering {
|
val gameTestServer by registering {
|
||||||
workingDirectory(file("run/testServer"))
|
workingDirectory(file("run/testServer"))
|
||||||
|
configureForGameTest()
|
||||||
|
|
||||||
property("forge.logging.console.level", "info")
|
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 }
|
val shade by registering { isTransitive = false }
|
||||||
implementation { extendsFrom(shade.get()) }
|
implementation { extendsFrom(shade.get()) }
|
||||||
register("cctJavadoc")
|
register("cctJavadoc")
|
||||||
|
|
||||||
val testModExtra by registering
|
|
||||||
named("testModImplementation") { extendsFrom(implementation.get(), testModExtra.get()) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@ -143,15 +145,13 @@ dependencies {
|
|||||||
|
|
||||||
"shade"(libs.cobalt)
|
"shade"(libs.cobalt)
|
||||||
|
|
||||||
|
testFixturesApi(libs.bundles.test)
|
||||||
|
testFixturesApi(libs.bundles.kotlin)
|
||||||
|
|
||||||
testImplementation(libs.bundles.test)
|
testImplementation(libs.bundles.test)
|
||||||
testImplementation(libs.bundles.kotlin)
|
testImplementation(libs.bundles.kotlin)
|
||||||
testRuntimeOnly(libs.bundles.testRuntime)
|
testRuntimeOnly(libs.bundles.testRuntime)
|
||||||
|
|
||||||
"testModImplementation"(sourceSets.main.get().output)
|
|
||||||
"testModExtra"(libs.bundles.kotlin) {
|
|
||||||
exclude("org.jetbrains", "annotations")
|
|
||||||
}
|
|
||||||
|
|
||||||
"cctJavadoc"(libs.cctJavadoc)
|
"cctJavadoc"(libs.cctJavadoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,7 +189,7 @@ val luaJavadoc by tasks.registering(Javadoc::class) {
|
|||||||
|
|
||||||
javadocTool.set(
|
javadocTool.set(
|
||||||
javaToolchains.javadocToolFor {
|
javaToolchains.javadocToolFor {
|
||||||
languageVersion.set(JavaLanguageVersion.of(17))
|
languageVersion.set(cc.tweaked.gradle.CCTweakedPlugin.JAVA_VERSION)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -219,7 +219,7 @@ tasks.jar {
|
|||||||
"Specification-Title" to "computercraft",
|
"Specification-Title" to "computercraft",
|
||||||
"Specification-Vendor" to "SquidDev",
|
"Specification-Vendor" to "SquidDev",
|
||||||
"Specification-Version" to "1",
|
"Specification-Version" to "1",
|
||||||
"specificationVersion" to "cctweaked",
|
"Implementation-Title" to "cctweaked",
|
||||||
"Implementation-Version" to modVersion,
|
"Implementation-Version" to modVersion,
|
||||||
"Implementation-Vendor" to "SquidDev",
|
"Implementation-Vendor" to "SquidDev",
|
||||||
)
|
)
|
||||||
@ -312,7 +312,7 @@ val docWebsite by tasks.registering(Copy::class) {
|
|||||||
// Check tasks
|
// Check tasks
|
||||||
|
|
||||||
tasks.test {
|
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) {
|
val lintLua by tasks.registering(IlluaminateExec::class) {
|
||||||
|
@ -9,6 +9,7 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(libs.kotlin.plugin)
|
||||||
implementation(libs.spotless)
|
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 cc.tweaked.gradle.LicenseHeader
|
||||||
import com.diffplug.gradle.spotless.FormatExtension
|
import com.diffplug.gradle.spotless.FormatExtension
|
||||||
import com.diffplug.spotless.LineEnding
|
import com.diffplug.spotless.LineEnding
|
||||||
@ -12,7 +13,7 @@ plugins {
|
|||||||
|
|
||||||
java {
|
java {
|
||||||
toolchain {
|
toolchain {
|
||||||
languageVersion.set(JavaLanguageVersion.of(17))
|
languageVersion.set(CCTweakedPlugin.JAVA_VERSION)
|
||||||
}
|
}
|
||||||
|
|
||||||
withSourcesJar()
|
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.Plugin
|
||||||
import org.gradle.api.Project
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures projects to match a shared configuration.
|
* Configures projects to match a shared configuration.
|
||||||
@ -10,4 +11,8 @@ class CCTweakedPlugin : Plugin<Project> {
|
|||||||
override fun apply(project: Project) {
|
override fun apply(project: Project) {
|
||||||
project.extensions.create("cct", CCTweakedExtension::class.java)
|
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"
|
junit = "5.9.1"
|
||||||
|
|
||||||
# Build tools
|
# Build tools
|
||||||
cctJavadoc = "1.5.1"
|
cctJavadoc = "1.5.2"
|
||||||
checkstyle = "10.3.4"
|
checkstyle = "10.3.4"
|
||||||
curseForgeGradle = "1.0.11"
|
curseForgeGradle = "1.0.11"
|
||||||
forgeGradle = "5.1.+"
|
forgeGradle = "5.1.+"
|
||||||
githubRelease = "2.2.12"
|
githubRelease = "2.2.12"
|
||||||
illuaminate = "0.1.0-3-g0f40379"
|
illuaminate = "0.1.0-7-g2a5a89c"
|
||||||
librarian = "1.+"
|
librarian = "1.+"
|
||||||
minotaur = "2.+"
|
minotaur = "2.+"
|
||||||
mixinGradle = "0.7.+"
|
mixinGradle = "0.7.+"
|
||||||
@ -49,6 +49,7 @@ junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", vers
|
|||||||
# Build tools
|
# Build tools
|
||||||
cctJavadoc = { module = "cc.tweaked:cct-javadoc", version.ref = "cctJavadoc" }
|
cctJavadoc = { module = "cc.tweaked:cct-javadoc", version.ref = "cctJavadoc" }
|
||||||
checkstyle = { module = "com.puppycrawl.tools:checkstyle", version.ref = "checkstyle" }
|
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" }
|
spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
@ -74,6 +74,8 @@ public final class ComputerCraft
|
|||||||
public static int monitorWidth = 8;
|
public static int monitorWidth = 8;
|
||||||
public static int monitorHeight = 6;
|
public static int monitorHeight = 6;
|
||||||
|
|
||||||
|
public static int uploadNagDelay = 5;
|
||||||
|
|
||||||
public static final Logger log = LoggerFactory.getLogger( MOD_ID );
|
public static final Logger log = LoggerFactory.getLogger( MOD_ID );
|
||||||
|
|
||||||
public ComputerCraft()
|
public ComputerCraft()
|
||||||
|
@ -17,30 +17,35 @@ import dan200.computercraft.shared.computer.inventory.ContainerComputerBase;
|
|||||||
import dan200.computercraft.shared.computer.upload.FileUpload;
|
import dan200.computercraft.shared.computer.upload.FileUpload;
|
||||||
import dan200.computercraft.shared.computer.upload.UploadResult;
|
import dan200.computercraft.shared.computer.upload.UploadResult;
|
||||||
import dan200.computercraft.shared.network.NetworkHandler;
|
import dan200.computercraft.shared.network.NetworkHandler;
|
||||||
import dan200.computercraft.shared.network.server.ContinueUploadMessage;
|
|
||||||
import dan200.computercraft.shared.network.server.UploadFileMessage;
|
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.client.gui.screens.inventory.AbstractContainerScreen;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.network.chat.TextComponent;
|
||||||
import net.minecraft.network.chat.TranslatableComponent;
|
import net.minecraft.network.chat.TranslatableComponent;
|
||||||
import net.minecraft.world.entity.player.Inventory;
|
import net.minecraft.world.entity.player.Inventory;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
import org.lwjgl.glfw.GLFW;
|
import org.lwjgl.glfw.GLFW;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.channels.SeekableByteChannel;
|
import java.nio.channels.SeekableByteChannel;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public abstract class ComputerScreenBase<T extends ContainerComputerBase> extends AbstractContainerScreen<T>
|
public abstract class ComputerScreenBase<T extends ContainerComputerBase> extends AbstractContainerScreen<T>
|
||||||
{
|
{
|
||||||
private static final Component OK = new TranslatableComponent( "gui.ok" );
|
private static final Component OK = new TranslatableComponent( "gui.ok" );
|
||||||
private static final Component CANCEL = new TranslatableComponent( "gui.cancel" );
|
private static final Component NO_RESPONSE_TITLE = new TranslatableComponent( "gui.computercraft.upload.no_response" );
|
||||||
private static final Component OVERWRITE = new TranslatableComponent( "gui.computercraft.upload.overwrite_button" );
|
private static final Component NO_RESPONSE_MSG = new TranslatableComponent( "gui.computercraft.upload.no_response.msg",
|
||||||
|
new TextComponent( "import" ).withStyle( ChatFormatting.DARK_GRAY ) );
|
||||||
|
|
||||||
protected WidgetTerminal terminal;
|
protected WidgetTerminal terminal;
|
||||||
protected Terminal terminalData;
|
protected Terminal terminalData;
|
||||||
@ -49,11 +54,15 @@ public abstract class ComputerScreenBase<T extends ContainerComputerBase> extend
|
|||||||
|
|
||||||
protected final int sidebarYOffset;
|
protected final int sidebarYOffset;
|
||||||
|
|
||||||
|
private long uploadNagDeadline = Long.MAX_VALUE;
|
||||||
|
private final ItemStack displayStack;
|
||||||
|
|
||||||
public ComputerScreenBase( T container, Inventory player, Component title, int sidebarYOffset )
|
public ComputerScreenBase( T container, Inventory player, Component title, int sidebarYOffset )
|
||||||
{
|
{
|
||||||
super( container, player, title );
|
super( container, player, title );
|
||||||
terminalData = container.getTerminal();
|
terminalData = container.getTerminal();
|
||||||
family = container.getFamily();
|
family = container.getFamily();
|
||||||
|
displayStack = container.getDisplayStack();
|
||||||
input = new ClientInputHandler( menu );
|
input = new ClientInputHandler( menu );
|
||||||
this.sidebarYOffset = sidebarYOffset;
|
this.sidebarYOffset = sidebarYOffset;
|
||||||
}
|
}
|
||||||
@ -83,6 +92,13 @@ public abstract class ComputerScreenBase<T extends ContainerComputerBase> extend
|
|||||||
{
|
{
|
||||||
super.containerTick();
|
super.containerTick();
|
||||||
terminal.update();
|
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
|
@Override
|
||||||
@ -194,41 +210,29 @@ public abstract class ComputerScreenBase<T extends ContainerComputerBase> extend
|
|||||||
if( toUpload.size() > 0 ) UploadFileMessage.send( menu, toUpload, NetworkHandler::sendToServer );
|
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 )
|
switch( result )
|
||||||
{
|
{
|
||||||
case SUCCESS:
|
case QUEUED:
|
||||||
alert( UploadResult.SUCCESS_TITLE, message );
|
{
|
||||||
|
if( ComputerCraft.uploadNagDelay > 0 )
|
||||||
|
{
|
||||||
|
uploadNagDeadline = Util.getNanos() + TimeUnit.SECONDS.toNanos( ComputerCraft.uploadNagDelay );
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
case CONSUMED:
|
||||||
|
{
|
||||||
|
uploadNagDeadline = Long.MAX_VALUE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case ERROR:
|
case ERROR:
|
||||||
alert( UploadResult.FAILED_TITLE, message );
|
alert( UploadResult.FAILED_TITLE, message );
|
||||||
break;
|
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 )
|
private void alert( Component title, Component message )
|
||||||
{
|
{
|
||||||
OptionScreen.show( minecraft, title, 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();
|
DateTimeFormatterBuilder formatter = new DateTimeFormatterBuilder();
|
||||||
LuaDateTime.format( formatter, format );
|
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;
|
package dan200.computercraft.core.computer;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.errorprone.annotations.concurrent.GuardedBy;
|
import com.google.errorprone.annotations.concurrent.GuardedBy;
|
||||||
import dan200.computercraft.ComputerCraft;
|
import dan200.computercraft.ComputerCraft;
|
||||||
import dan200.computercraft.core.ComputerContext;
|
import dan200.computercraft.core.ComputerContext;
|
||||||
@ -443,7 +444,8 @@ public final class ComputerThread
|
|||||||
*
|
*
|
||||||
* @return If we have work queued up.
|
* @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!
|
// FIXME: See comment in scaledPeriod. Again, we access this in multiple threads but not clear if it matters!
|
||||||
return !computerQueue.isEmpty();
|
return !computerQueue.isEmpty();
|
||||||
|
@ -295,6 +295,7 @@ public final class ResourceMount implements IMount
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
for( ResourceMount mount : MOUNT_CACHE.values() ) mount.load( manager );
|
for( ResourceMount mount : MOUNT_CACHE.values() ) mount.load( manager );
|
||||||
|
CONTENTS_CACHE.invalidateAll();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
@ -16,7 +16,6 @@ import dan200.computercraft.core.apis.http.options.Action;
|
|||||||
import dan200.computercraft.core.apis.http.options.AddressRuleConfig;
|
import dan200.computercraft.core.apis.http.options.AddressRuleConfig;
|
||||||
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
|
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
|
||||||
import net.minecraftforge.common.ForgeConfigSpec;
|
import net.minecraftforge.common.ForgeConfigSpec;
|
||||||
import net.minecraftforge.common.ForgeConfigSpec.Builder;
|
|
||||||
import net.minecraftforge.common.ForgeConfigSpec.ConfigValue;
|
import net.minecraftforge.common.ForgeConfigSpec.ConfigValue;
|
||||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||||
import net.minecraftforge.fml.ModLoadingContext;
|
import net.minecraftforge.fml.ModLoadingContext;
|
||||||
@ -83,6 +82,7 @@ public final class Config
|
|||||||
|
|
||||||
private static final ConfigValue<MonitorRenderer> monitorRenderer;
|
private static final ConfigValue<MonitorRenderer> monitorRenderer;
|
||||||
private static final ConfigValue<Integer> monitorDistance;
|
private static final ConfigValue<Integer> monitorDistance;
|
||||||
|
private static final ConfigValue<Integer> uploadNagDelay;
|
||||||
|
|
||||||
private static final ForgeConfigSpec serverSpec;
|
private static final ForgeConfigSpec serverSpec;
|
||||||
private static final ForgeConfigSpec clientSpec;
|
private static final ForgeConfigSpec clientSpec;
|
||||||
@ -91,7 +91,7 @@ public final class Config
|
|||||||
|
|
||||||
static
|
static
|
||||||
{
|
{
|
||||||
Builder builder = new Builder();
|
ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder();
|
||||||
|
|
||||||
{ // General computers
|
{ // General computers
|
||||||
computerSpaceLimit = builder
|
computerSpaceLimit = builder
|
||||||
@ -275,13 +275,17 @@ public final class Config
|
|||||||
|
|
||||||
serverSpec = builder.build();
|
serverSpec = builder.build();
|
||||||
|
|
||||||
Builder clientBuilder = new Builder();
|
ForgeConfigSpec.Builder clientBuilder = new ForgeConfigSpec.Builder();
|
||||||
monitorRenderer = clientBuilder
|
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." )
|
.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 );
|
.defineEnum( "monitor_renderer", MonitorRenderer.BEST );
|
||||||
monitorDistance = clientBuilder
|
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." )
|
.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 );
|
.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();
|
clientSpec = clientBuilder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -347,6 +351,7 @@ public final class Config
|
|||||||
// Client
|
// Client
|
||||||
ComputerCraft.monitorRenderer = monitorRenderer.get();
|
ComputerCraft.monitorRenderer = monitorRenderer.get();
|
||||||
ComputerCraft.monitorDistance = monitorDistance.get();
|
ComputerCraft.monitorDistance = monitorDistance.get();
|
||||||
|
ComputerCraft.uploadNagDelay = uploadNagDelay.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@SubscribeEvent
|
@SubscribeEvent
|
||||||
|
@ -35,6 +35,7 @@ import net.minecraft.world.entity.Entity;
|
|||||||
import net.minecraft.world.entity.player.Inventory;
|
import net.minecraft.world.entity.player.Inventory;
|
||||||
import net.minecraft.world.entity.player.Player;
|
import net.minecraft.world.entity.player.Player;
|
||||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.minecraft.world.level.Level;
|
import net.minecraft.world.level.Level;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
@ -224,7 +225,7 @@ public final class CommandComputerCraft
|
|||||||
.executes( context -> {
|
.executes( context -> {
|
||||||
ServerPlayer player = context.getSource().getPlayerOrException();
|
ServerPlayer player = context.getSource().getPlayerOrException();
|
||||||
ServerComputer computer = getComputerArgument( context, "computer" );
|
ServerComputer computer = getComputerArgument( context, "computer" );
|
||||||
new ComputerContainerData( computer ).open( player, new MenuProvider()
|
new ComputerContainerData( computer, ItemStack.EMPTY ).open( player, new MenuProvider()
|
||||||
{
|
{
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
|
@ -19,17 +19,9 @@ import net.minecraft.world.level.block.state.BlockState;
|
|||||||
import net.minecraft.world.phys.BlockHitResult;
|
import net.minecraft.world.phys.BlockHitResult;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
public abstract class TileGeneric extends BlockEntity
|
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 )
|
public TileGeneric( BlockEntityType<? extends TileGeneric> type, BlockPos pos, BlockState state )
|
||||||
{
|
{
|
||||||
super( type, pos, state );
|
super( type, pos, state );
|
||||||
|
@ -141,7 +141,11 @@ public abstract class TileComputerBase extends TileGeneric implements IComputerT
|
|||||||
{
|
{
|
||||||
ServerComputer computer = createServerComputer();
|
ServerComputer computer = createServerComputer();
|
||||||
computer.turnOn();
|
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;
|
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.ComputerFamily;
|
||||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||||
import dan200.computercraft.shared.computer.menu.ComputerMenu;
|
import dan200.computercraft.shared.computer.menu.ComputerMenu;
|
||||||
|
import dan200.computercraft.shared.computer.menu.ServerInputHandler;
|
||||||
import dan200.computercraft.shared.computer.menu.ServerInputState;
|
import dan200.computercraft.shared.computer.menu.ServerInputState;
|
||||||
import dan200.computercraft.shared.network.client.TerminalState;
|
import dan200.computercraft.shared.network.client.TerminalState;
|
||||||
import dan200.computercraft.shared.network.container.ComputerContainerData;
|
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.ContainerData;
|
||||||
import net.minecraft.world.inventory.MenuType;
|
import net.minecraft.world.inventory.MenuType;
|
||||||
import net.minecraft.world.inventory.SimpleContainerData;
|
import net.minecraft.world.inventory.SimpleContainerData;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
@ -30,10 +32,12 @@ public abstract class ContainerComputerBase extends AbstractContainerMenu implem
|
|||||||
private final ContainerData data;
|
private final ContainerData data;
|
||||||
|
|
||||||
private final @Nullable ServerComputer computer;
|
private final @Nullable ServerComputer computer;
|
||||||
private final @Nullable ServerInputState input;
|
private final @Nullable ServerInputState<ContainerComputerBase> input;
|
||||||
|
|
||||||
private final @Nullable Terminal terminal;
|
private final @Nullable Terminal terminal;
|
||||||
|
|
||||||
|
private final ItemStack displayStack;
|
||||||
|
|
||||||
public ContainerComputerBase(
|
public ContainerComputerBase(
|
||||||
MenuType<? extends ContainerComputerBase> type, int id, Predicate<Player> canUse,
|
MenuType<? extends ContainerComputerBase> type, int id, Predicate<Player> canUse,
|
||||||
ComputerFamily family, @Nullable ServerComputer computer, @Nullable ComputerContainerData containerData
|
ComputerFamily family, @Nullable ServerComputer computer, @Nullable ComputerContainerData containerData
|
||||||
@ -46,8 +50,9 @@ public abstract class ContainerComputerBase extends AbstractContainerMenu implem
|
|||||||
addDataSlots( data );
|
addDataSlots( data );
|
||||||
|
|
||||||
this.computer = computer;
|
this.computer = computer;
|
||||||
input = computer == null ? null : new ServerInputState( this );
|
input = computer == null ? null : new ServerInputState<>( this );
|
||||||
terminal = containerData == null ? null : containerData.terminal().create();
|
terminal = containerData == null ? null : containerData.terminal().create();
|
||||||
|
displayStack = containerData == null ? null : containerData.displayStack();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -75,7 +80,7 @@ public abstract class ContainerComputerBase extends AbstractContainerMenu implem
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ServerInputState getInput()
|
public ServerInputHandler getInput()
|
||||||
{
|
{
|
||||||
if( input == null ) throw new UnsupportedOperationException( "Cannot access server computer on the client" );
|
if( input == null ) throw new UnsupportedOperationException( "Cannot access server computer on the client" );
|
||||||
return input;
|
return input;
|
||||||
@ -106,4 +111,15 @@ public abstract class ContainerComputerBase extends AbstractContainerMenu implem
|
|||||||
super.removed( player );
|
super.removed( player );
|
||||||
if( input != null ) input.close();
|
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.
|
* @param uploadId The unique ID of this upload.
|
||||||
*/
|
*/
|
||||||
void finishUpload( @Nonnull ServerPlayer uploader, @Nonnull UUID uploadId );
|
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;
|
package dan200.computercraft.shared.computer.menu;
|
||||||
|
|
||||||
import dan200.computercraft.ComputerCraft;
|
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.core.ServerComputer;
|
||||||
import dan200.computercraft.shared.computer.upload.FileSlice;
|
import dan200.computercraft.shared.computer.upload.*;
|
||||||
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.NetworkHandler;
|
||||||
import dan200.computercraft.shared.network.NetworkMessage;
|
import dan200.computercraft.shared.network.NetworkMessage;
|
||||||
import dan200.computercraft.shared.network.client.UploadResultMessage;
|
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 it.unimi.dsi.fastutil.ints.IntSet;
|
||||||
import net.minecraft.network.chat.TranslatableComponent;
|
import net.minecraft.network.chat.TranslatableComponent;
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.channels.WritableByteChannel;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.StringJoiner;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.Function;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default concrete implementation of {@link ServerInputHandler}.
|
* The default concrete implementation of {@link ServerInputHandler}.
|
||||||
* <p>
|
* <p>
|
||||||
* This keeps track of the current key and mouse state, and releases them when the container is closed.
|
* 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 T owner;
|
||||||
|
|
||||||
private final ComputerMenu owner;
|
|
||||||
private final IntSet keysDown = new IntOpenHashSet( 4 );
|
private final IntSet keysDown = new IntOpenHashSet( 4 );
|
||||||
|
|
||||||
private int lastMouseX;
|
private int lastMouseX;
|
||||||
@ -50,7 +42,7 @@ public class ServerInputState implements ServerInputHandler
|
|||||||
private @Nullable UUID toUploadId;
|
private @Nullable UUID toUploadId;
|
||||||
private @Nullable List<FileUpload> toUpload;
|
private @Nullable List<FileUpload> toUpload;
|
||||||
|
|
||||||
public ServerInputState( ComputerMenu owner )
|
public ServerInputState( T owner )
|
||||||
{
|
{
|
||||||
this.owner = owner;
|
this.owner = owner;
|
||||||
}
|
}
|
||||||
@ -160,91 +152,31 @@ public class ServerInputState implements ServerInputHandler
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
NetworkMessage message = finishUpload( false );
|
NetworkMessage message = finishUpload( uploader );
|
||||||
NetworkHandler.sendToPlayer( uploader, message );
|
NetworkHandler.sendToPlayer( uploader, message );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private UploadResultMessage finishUpload( ServerPlayer player )
|
||||||
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 )
|
|
||||||
{
|
{
|
||||||
ServerComputer computer = owner.getComputer();
|
ServerComputer computer = owner.getComputer();
|
||||||
if( toUpload == null ) return UploadResultMessage.COMPUTER_OFF;
|
if( toUpload == null )
|
||||||
|
{
|
||||||
FileSystem fs = computer.getComputer().getAPIEnvironment().getFileSystem();
|
return UploadResultMessage.error( owner, UploadResult.COMPUTER_OFF_MSG );
|
||||||
|
}
|
||||||
|
|
||||||
for( FileUpload upload : toUpload )
|
for( FileUpload upload : toUpload )
|
||||||
{
|
{
|
||||||
if( !upload.checksumMatches() )
|
if( !upload.checksumMatches() )
|
||||||
{
|
{
|
||||||
ComputerCraft.log.warn( "Checksum failed to match for {}.", upload.getName() );
|
ComputerCraft.log.warn( "Checksum failed to match for {}.", upload.getName() );
|
||||||
return new UploadResultMessage( UploadResult.ERROR, new TranslatableComponent( "gui.computercraft.upload.failed.corrupted" ) );
|
return UploadResultMessage.error( owner, new TranslatableComponent( "gui.computercraft.upload.failed.corrupted" ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
computer.queueEvent( "file_transfer", new Object[] {
|
||||||
{
|
new TransferredFiles( player, owner, toUpload.stream().map( x -> new TransferredFile( x.getName(), x.getBytes() ) ).collect( Collectors.toList() ) ),
|
||||||
List<String> overwrite = new ArrayList<>();
|
} );
|
||||||
List<FileUpload> files = toUpload;
|
return UploadResultMessage.queued( owner );
|
||||||
toUpload = null;
|
|
||||||
for( FileUpload upload : files )
|
|
||||||
{
|
|
||||||
if( !fs.exists( upload.getName() ) ) continue;
|
|
||||||
if( fs.isDir( upload.getName() ) )
|
|
||||||
{
|
|
||||||
return new UploadResultMessage(
|
|
||||||
UploadResult.ERROR,
|
|
||||||
new TranslatableComponent( "gui.computercraft.upload.failed.overwrite_dir", upload.getName() )
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
overwrite.add( upload.getName() );
|
|
||||||
}
|
|
||||||
|
|
||||||
if( !overwrite.isEmpty() && !forceOverwrite )
|
|
||||||
{
|
|
||||||
StringJoiner joiner = new StringJoiner( LIST_PREFIX, LIST_PREFIX, "" );
|
|
||||||
for( String value : overwrite ) joiner.add( value );
|
|
||||||
toUpload = files;
|
|
||||||
return new UploadResultMessage(
|
|
||||||
UploadResult.CONFIRM_OVERWRITE,
|
|
||||||
new TranslatableComponent( "gui.computercraft.upload.overwrite.detail", joiner.toString() )
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
long availableSpace = fs.getFreeSpace( "/" );
|
|
||||||
long neededSpace = 0;
|
|
||||||
for( FileUpload upload : files ) neededSpace += Math.max( 512, upload.getBytes().remaining() );
|
|
||||||
if( neededSpace > availableSpace ) return UploadResultMessage.OUT_OF_SPACE;
|
|
||||||
|
|
||||||
for( FileUpload file : files )
|
|
||||||
{
|
|
||||||
try( FileSystemWrapper<WritableByteChannel> channel = fs.openForWrite( file.getName(), false, Function.identity() ) )
|
|
||||||
{
|
|
||||||
channel.get().write( file.getBytes() );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new UploadResultMessage(
|
|
||||||
UploadResult.SUCCESS, new TranslatableComponent( "gui.computercraft.upload.success.msg", files.size() )
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch( FileSystemException | IOException e )
|
|
||||||
{
|
|
||||||
ComputerCraft.log.error( "Error uploading files", e );
|
|
||||||
return new UploadResultMessage( UploadResult.ERROR, new TranslatableComponent( "gui.computercraft.upload.failed.generic", e.getMessage() ) );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close()
|
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 ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -10,16 +10,13 @@ import net.minecraft.network.chat.TranslatableComponent;
|
|||||||
|
|
||||||
public enum UploadResult
|
public enum UploadResult
|
||||||
{
|
{
|
||||||
SUCCESS,
|
QUEUED,
|
||||||
ERROR,
|
CONSUMED,
|
||||||
CONFIRM_OVERWRITE;
|
ERROR;
|
||||||
|
|
||||||
public static final Component SUCCESS_TITLE = new TranslatableComponent( "gui.computercraft.upload.success" );
|
public static final Component SUCCESS_TITLE = new TranslatableComponent( "gui.computercraft.upload.success" );
|
||||||
|
|
||||||
public static final Component FAILED_TITLE = new TranslatableComponent( "gui.computercraft.upload.failed" );
|
public static final Component FAILED_TITLE = new TranslatableComponent( "gui.computercraft.upload.failed" );
|
||||||
public static final Component COMPUTER_OFF_MSG = new TranslatableComponent( "gui.computercraft.upload.failed.computer_off" );
|
public static final Component COMPUTER_OFF_MSG = new TranslatableComponent( "gui.computercraft.upload.failed.computer_off" );
|
||||||
public static final Component OUT_OF_SPACE_MSG = new TranslatableComponent( "gui.computercraft.upload.failed.out_of_space" );
|
|
||||||
public static final Component TOO_MUCH_MSG = new TranslatableComponent( "gui.computercraft.upload.failed.too_much" );
|
public static final Component TOO_MUCH_MSG = new TranslatableComponent( "gui.computercraft.upload.failed.too_much" );
|
||||||
|
|
||||||
public static final Component UPLOAD_OVERWRITE = new TranslatableComponent( "gui.computercraft.upload.overwrite" );
|
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,6 @@ public final class NetworkHandler
|
|||||||
registerMainThread( 2, NetworkDirection.PLAY_TO_SERVER, KeyEventServerMessage.class, KeyEventServerMessage::new );
|
registerMainThread( 2, NetworkDirection.PLAY_TO_SERVER, KeyEventServerMessage.class, KeyEventServerMessage::new );
|
||||||
registerMainThread( 3, NetworkDirection.PLAY_TO_SERVER, MouseEventServerMessage.class, MouseEventServerMessage::new );
|
registerMainThread( 3, NetworkDirection.PLAY_TO_SERVER, MouseEventServerMessage.class, MouseEventServerMessage::new );
|
||||||
registerMainThread( 4, NetworkDirection.PLAY_TO_SERVER, UploadFileMessage.class, UploadFileMessage::new );
|
registerMainThread( 4, NetworkDirection.PLAY_TO_SERVER, UploadFileMessage.class, UploadFileMessage::new );
|
||||||
registerMainThread( 5, NetworkDirection.PLAY_TO_SERVER, ContinueUploadMessage.class, ContinueUploadMessage::new );
|
|
||||||
|
|
||||||
// Client messages
|
// Client messages
|
||||||
registerMainThread( 10, NetworkDirection.PLAY_TO_CLIENT, ChatTableClientMessage.class, ChatTableClientMessage::new );
|
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.client.gui.screens.Screen;
|
||||||
import net.minecraft.network.FriendlyByteBuf;
|
import net.minecraft.network.FriendlyByteBuf;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||||
import net.minecraftforge.network.NetworkEvent;
|
import net.minecraftforge.network.NetworkEvent;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
public class UploadResultMessage implements NetworkMessage
|
public class UploadResultMessage implements NetworkMessage
|
||||||
{
|
{
|
||||||
public static final UploadResultMessage COMPUTER_OFF = new UploadResultMessage( UploadResult.ERROR, UploadResult.COMPUTER_OFF_MSG );
|
private final int containerId;
|
||||||
public static final UploadResultMessage OUT_OF_SPACE = new UploadResultMessage( UploadResult.ERROR, UploadResult.OUT_OF_SPACE_MSG );
|
|
||||||
|
|
||||||
private final UploadResult result;
|
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.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 )
|
public UploadResultMessage( @Nonnull FriendlyByteBuf buf )
|
||||||
{
|
{
|
||||||
|
containerId = buf.readVarInt();
|
||||||
result = buf.readEnum( UploadResult.class );
|
result = buf.readEnum( UploadResult.class );
|
||||||
message = buf.readComponent();
|
errorMessage = result == UploadResult.ERROR ? buf.readComponent() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void toBytes( @Nonnull FriendlyByteBuf buf )
|
public void toBytes( @Nonnull FriendlyByteBuf buf )
|
||||||
{
|
{
|
||||||
|
buf.writeVarInt( containerId );
|
||||||
buf.writeEnum( result );
|
buf.writeEnum( result );
|
||||||
buf.writeComponent( message );
|
if( result == UploadResult.ERROR ) buf.writeComponent( errorMessage );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -50,9 +68,9 @@ public class UploadResultMessage implements NetworkMessage
|
|||||||
Minecraft minecraft = Minecraft.getInstance();
|
Minecraft minecraft = Minecraft.getInstance();
|
||||||
|
|
||||||
Screen screen = OptionScreen.unwrap( minecraft.screen );
|
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.computer.core.ServerComputer;
|
||||||
import dan200.computercraft.shared.network.client.TerminalState;
|
import dan200.computercraft.shared.network.client.TerminalState;
|
||||||
import net.minecraft.network.FriendlyByteBuf;
|
import net.minecraft.network.FriendlyByteBuf;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
public class ComputerContainerData implements ContainerData
|
public class ComputerContainerData implements ContainerData
|
||||||
{
|
{
|
||||||
private final ComputerFamily family;
|
private final ComputerFamily family;
|
||||||
private final TerminalState terminal;
|
private final TerminalState terminal;
|
||||||
|
private final ItemStack displayStack;
|
||||||
|
|
||||||
public ComputerContainerData( ServerComputer computer )
|
public ComputerContainerData( ServerComputer computer, @Nonnull ItemStack displayStack )
|
||||||
{
|
{
|
||||||
family = computer.getFamily();
|
family = computer.getFamily();
|
||||||
terminal = computer.getTerminalState();
|
terminal = computer.getTerminalState();
|
||||||
|
this.displayStack = displayStack;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ComputerContainerData( FriendlyByteBuf buf )
|
public ComputerContainerData( FriendlyByteBuf buf )
|
||||||
{
|
{
|
||||||
family = buf.readEnum( ComputerFamily.class );
|
family = buf.readEnum( ComputerFamily.class );
|
||||||
terminal = new TerminalState( buf );
|
terminal = new TerminalState( buf );
|
||||||
|
displayStack = buf.readItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -32,6 +38,7 @@ public class ComputerContainerData implements ContainerData
|
|||||||
{
|
{
|
||||||
buf.writeEnum( family );
|
buf.writeEnum( family );
|
||||||
terminal.write( buf );
|
terminal.write( buf );
|
||||||
|
buf.writeItemStack( displayStack, true );
|
||||||
}
|
}
|
||||||
|
|
||||||
public ComputerFamily family()
|
public ComputerFamily family()
|
||||||
@ -43,4 +50,15 @@ public class ComputerContainerData implements ContainerData
|
|||||||
{
|
{
|
||||||
return terminal;
|
return terminal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a stack associated with this menu. This may be displayed on the client.
|
||||||
|
*
|
||||||
|
* @return The stack associated with this menu.
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
public ItemStack displayStack()
|
||||||
|
{
|
||||||
|
return displayStack;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 final WiredModemElement cable = new CableElement();
|
||||||
private LazyOptional<IWiredElement> elementCap;
|
private LazyOptional<IWiredElement> elementCap;
|
||||||
private final IWiredNode node = cable.getNode();
|
private final IWiredNode node = cable.getNode();
|
||||||
|
private final TickScheduler.Token tickToken = new TickScheduler.Token( this );
|
||||||
private final WiredModemPeripheral modem = new WiredModemPeripheral(
|
private final WiredModemPeripheral modem = new WiredModemPeripheral(
|
||||||
new ModemState( () -> TickScheduler.schedule( this ) ),
|
new ModemState( () -> TickScheduler.schedule( tickToken ) ),
|
||||||
cable
|
cable
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@ -168,7 +169,7 @@ public class TileCable extends TileGeneric
|
|||||||
public void clearRemoved()
|
public void clearRemoved()
|
||||||
{
|
{
|
||||||
super.clearRemoved(); // TODO: Replace with onLoad
|
super.clearRemoved(); // TODO: Replace with onLoad
|
||||||
TickScheduler.schedule( this );
|
TickScheduler.schedule( tickToken );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -241,7 +242,7 @@ public class TileCable extends TileGeneric
|
|||||||
{
|
{
|
||||||
if( invalidPeripheral ) return;
|
if( invalidPeripheral ) return;
|
||||||
invalidPeripheral = true;
|
invalidPeripheral = true;
|
||||||
TickScheduler.schedule( this );
|
TickScheduler.schedule( tickToken );
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshPeripheral()
|
private void refreshPeripheral()
|
||||||
|
@ -100,7 +100,8 @@ public class TileWiredModemFull extends TileGeneric
|
|||||||
private boolean destroyed = false;
|
private boolean destroyed = false;
|
||||||
private boolean connectionsFormed = 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 final WiredModemElement element = new FullElement( this );
|
||||||
private LazyOptional<IWiredElement> elementCap;
|
private LazyOptional<IWiredElement> elementCap;
|
||||||
private final IWiredNode node = element.getNode();
|
private final IWiredNode node = element.getNode();
|
||||||
@ -181,7 +182,7 @@ public class TileWiredModemFull extends TileGeneric
|
|||||||
|
|
||||||
private void queueRefreshPeripheral( @Nonnull Direction facing )
|
private void queueRefreshPeripheral( @Nonnull Direction facing )
|
||||||
{
|
{
|
||||||
if( invalidSides == 0 ) TickScheduler.schedule( this );
|
if( invalidSides == 0 ) TickScheduler.schedule( tickToken );
|
||||||
invalidSides |= 1 << facing.ordinal();
|
invalidSides |= 1 << facing.ordinal();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,7 +263,7 @@ public class TileWiredModemFull extends TileGeneric
|
|||||||
public void clearRemoved()
|
public void clearRemoved()
|
||||||
{
|
{
|
||||||
super.clearRemoved(); // TODO: Replace with onLoad
|
super.clearRemoved(); // TODO: Replace with onLoad
|
||||||
TickScheduler.schedule( this );
|
TickScheduler.schedule( tickToken );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -72,6 +72,13 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements IW
|
|||||||
protected abstract WiredModemLocalPeripheral getLocalPeripheral();
|
protected abstract WiredModemLocalPeripheral getLocalPeripheral();
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public Set<String> getAdditionalTypes()
|
||||||
|
{
|
||||||
|
return Collections.singleton( "peripheral_hub" );
|
||||||
|
}
|
||||||
|
|
||||||
//region Peripheral methods
|
//region Peripheral methods
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -33,7 +33,7 @@ public class TileWirelessModem extends TileGeneric
|
|||||||
|
|
||||||
Peripheral( TileWirelessModem entity )
|
Peripheral( TileWirelessModem entity )
|
||||||
{
|
{
|
||||||
super( new ModemState( () -> TickScheduler.schedule( entity ) ), entity.advanced );
|
super( new ModemState( () -> TickScheduler.schedule( entity.tickToken ) ), entity.advanced );
|
||||||
this.entity = entity;
|
this.entity = entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,6 +70,7 @@ public class TileWirelessModem extends TileGeneric
|
|||||||
private final ModemPeripheral modem;
|
private final ModemPeripheral modem;
|
||||||
private boolean destroyed = false;
|
private boolean destroyed = false;
|
||||||
private LazyOptional<IPeripheral> modemCap;
|
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 )
|
public TileWirelessModem( BlockEntityType<? extends TileWirelessModem> type, BlockPos pos, BlockState state, boolean advanced )
|
||||||
{
|
{
|
||||||
@ -82,7 +83,7 @@ public class TileWirelessModem extends TileGeneric
|
|||||||
public void clearRemoved()
|
public void clearRemoved()
|
||||||
{
|
{
|
||||||
super.clearRemoved(); // TODO: Replace with onLoad
|
super.clearRemoved(); // TODO: Replace with onLoad
|
||||||
TickScheduler.schedule( this );
|
TickScheduler.schedule( tickToken );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -64,7 +64,7 @@ public class ServerMonitor
|
|||||||
|
|
||||||
private void markChanged()
|
private void markChanged()
|
||||||
{
|
{
|
||||||
if( !changed.getAndSet( true ) ) TickScheduler.schedule( origin );
|
if( !changed.getAndSet( true ) ) TickScheduler.schedule( origin.tickToken );
|
||||||
}
|
}
|
||||||
|
|
||||||
int getTextScale()
|
int getTextScale()
|
||||||
|
@ -75,6 +75,8 @@ public class TileMonitor extends TileGeneric
|
|||||||
private int bbX, bbY, bbWidth, bbHeight;
|
private int bbX, bbY, bbWidth, bbHeight;
|
||||||
private AABB boundingBox;
|
private AABB boundingBox;
|
||||||
|
|
||||||
|
TickScheduler.Token tickToken = new TickScheduler.Token( this );
|
||||||
|
|
||||||
public TileMonitor( BlockEntityType<? extends TileMonitor> type, BlockPos pos, BlockState state, boolean advanced )
|
public TileMonitor( BlockEntityType<? extends TileMonitor> type, BlockPos pos, BlockState state, boolean advanced )
|
||||||
{
|
{
|
||||||
super( type, pos, state );
|
super( type, pos, state );
|
||||||
@ -86,7 +88,7 @@ public class TileMonitor extends TileGeneric
|
|||||||
{
|
{
|
||||||
super.clearRemoved();
|
super.clearRemoved();
|
||||||
needsValidating = true; // Same, tbh
|
needsValidating = true; // Same, tbh
|
||||||
TickScheduler.schedule( this );
|
TickScheduler.schedule( tickToken );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -165,7 +165,7 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I
|
|||||||
if( !stop )
|
if( !stop )
|
||||||
{
|
{
|
||||||
boolean isTypingOnly = hand == InteractionHand.OFF_HAND;
|
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 );
|
return new InteractionResultHolder<>( InteractionResult.SUCCESS, stack );
|
||||||
|
@ -6,17 +6,18 @@
|
|||||||
package dan200.computercraft.shared.util;
|
package dan200.computercraft.shared.util;
|
||||||
|
|
||||||
import dan200.computercraft.ComputerCraft;
|
import dan200.computercraft.ComputerCraft;
|
||||||
import dan200.computercraft.shared.common.TileGeneric;
|
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.world.level.Level;
|
import net.minecraft.world.level.Level;
|
||||||
import net.minecraft.world.level.LevelAccessor;
|
import net.minecraft.world.level.LevelAccessor;
|
||||||
import net.minecraft.world.level.block.Block;
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||||
import net.minecraftforge.event.TickEvent;
|
import net.minecraftforge.event.TickEvent;
|
||||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||||
import net.minecraftforge.fml.common.Mod;
|
import net.minecraftforge.fml.common.Mod;
|
||||||
|
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A thread-safe version of {@link LevelAccessor#scheduleTick(BlockPos, Block, int)}.
|
* 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();
|
Level world = token.owner.getLevel();
|
||||||
if( world != null && !world.isClientSide && !tile.scheduled.getAndSet( true ) ) toTick.add( tile );
|
if( world != null && !world.isClientSide && !token.scheduled.getAndSet( true ) ) toTick.add( token );
|
||||||
}
|
}
|
||||||
|
|
||||||
@SubscribeEvent
|
@SubscribeEvent
|
||||||
@ -43,19 +44,37 @@ public final class TickScheduler
|
|||||||
{
|
{
|
||||||
if( event.phase != TickEvent.Phase.START ) return;
|
if( event.phase != TickEvent.Phase.START ) return;
|
||||||
|
|
||||||
TileGeneric tile;
|
Token token;
|
||||||
while( (tile = toTick.poll()) != null )
|
while( (token = toTick.poll()) != null )
|
||||||
{
|
{
|
||||||
tile.scheduled.set( false );
|
token.scheduled.set( false );
|
||||||
if( tile.isRemoved() ) continue;
|
BlockEntity blockEntity = token.owner;
|
||||||
|
if( blockEntity.isRemoved() ) continue;
|
||||||
|
|
||||||
Level world = tile.getLevel();
|
Level world = blockEntity.getLevel();
|
||||||
BlockPos pos = tile.getBlockPos();
|
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": "Upload Succeeded",
|
||||||
"gui.computercraft.upload.success.msg": "%d files uploaded.",
|
"gui.computercraft.upload.success.msg": "%d files uploaded.",
|
||||||
"gui.computercraft.upload.failed": "Upload Failed",
|
"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.computer_off": "You must turn the computer on before uploading files.",
|
||||||
"gui.computercraft.upload.failed.too_much": "Your files are too large to be uploaded.",
|
"gui.computercraft.upload.failed.too_much": "Your files are too large to be uploaded.",
|
||||||
"gui.computercraft.upload.failed.name_too_long": "File names are too long 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.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.generic": "Uploading files failed (%s)",
|
||||||
"gui.computercraft.upload.failed.corrupted": "Files corrupted when uploading. Please try again.",
|
"gui.computercraft.upload.failed.corrupted": "Files corrupted when uploading. Please try again.",
|
||||||
"gui.computercraft.upload.overwrite": "Files would be overwritten",
|
"gui.computercraft.upload.no_response": "Transferring Files",
|
||||||
"gui.computercraft.upload.overwrite.detail": "The following files will be overwritten when uploading. Continue?%s",
|
"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.upload.overwrite_button": "Overwrite",
|
|
||||||
"gui.computercraft.pocket_computer_overlay": "Pocket computer open. Press ESC to close."
|
"gui.computercraft.pocket_computer_overlay": "Pocket computer open. Press ESC to close."
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,7 @@ function getNames()
|
|||||||
local side = sides[n]
|
local side = sides[n]
|
||||||
if native.isPresent(side) then
|
if native.isPresent(side) then
|
||||||
table.insert(results, side)
|
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")
|
local remote = native.call(side, "getNamesRemote")
|
||||||
for _, name in ipairs(remote) do
|
for _, name in ipairs(remote) do
|
||||||
table.insert(results, name)
|
table.insert(results, name)
|
||||||
@ -134,9 +134,7 @@ function isPresent(name)
|
|||||||
|
|
||||||
for n = 1, #sides do
|
for n = 1, #sides do
|
||||||
local side = sides[n]
|
local side = sides[n]
|
||||||
if native.hasType(side, "modem") and not native.call(side, "isWireless") and
|
if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", name) then
|
||||||
native.call(side, "isPresentRemote", name)
|
|
||||||
then
|
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -162,9 +160,7 @@ function getType(peripheral)
|
|||||||
end
|
end
|
||||||
for n = 1, #sides do
|
for n = 1, #sides do
|
||||||
local side = sides[n]
|
local side = sides[n]
|
||||||
if native.hasType(side, "modem") and not native.call(side, "isWireless") and
|
if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", peripheral) then
|
||||||
native.call(side, "isPresentRemote", peripheral)
|
|
||||||
then
|
|
||||||
return native.call(side, "getTypeRemote", peripheral)
|
return native.call(side, "getTypeRemote", peripheral)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -195,9 +191,7 @@ function hasType(peripheral, peripheral_type)
|
|||||||
end
|
end
|
||||||
for n = 1, #sides do
|
for n = 1, #sides do
|
||||||
local side = sides[n]
|
local side = sides[n]
|
||||||
if native.hasType(side, "modem") and not native.call(side, "isWireless") and
|
if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", peripheral) then
|
||||||
native.call(side, "isPresentRemote", peripheral)
|
|
||||||
then
|
|
||||||
return native.call(side, "hasTypeRemote", peripheral, peripheral_type)
|
return native.call(side, "hasTypeRemote", peripheral, peripheral_type)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -223,9 +217,7 @@ function getMethods(name)
|
|||||||
end
|
end
|
||||||
for n = 1, #sides do
|
for n = 1, #sides do
|
||||||
local side = sides[n]
|
local side = sides[n]
|
||||||
if native.hasType(side, "modem") and not native.call(side, "isWireless") and
|
if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", name) then
|
||||||
native.call(side, "isPresentRemote", name)
|
|
||||||
then
|
|
||||||
return native.call(side, "getMethodsRemote", name)
|
return native.call(side, "getMethodsRemote", name)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -265,9 +257,7 @@ function call(name, method, ...)
|
|||||||
|
|
||||||
for n = 1, #sides do
|
for n = 1, #sides do
|
||||||
local side = sides[n]
|
local side = sides[n]
|
||||||
if native.hasType(side, "modem") and not native.call(side, "isWireless") and
|
if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", name) then
|
||||||
native.call(side, "isPresentRemote", name)
|
|
||||||
then
|
|
||||||
return native.call(side, "callRemote", name, method, ...)
|
return native.call(side, "callRemote", name, method, ...)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -211,6 +211,8 @@ local function tabulateCommon(bPaged, ...)
|
|||||||
end
|
end
|
||||||
print()
|
print()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local previous_colour = term.getTextColour()
|
||||||
for _, t in ipairs(tAll) do
|
for _, t in ipairs(tAll) do
|
||||||
if type(t) == "table" then
|
if type(t) == "table" then
|
||||||
if #t > 0 then
|
if #t > 0 then
|
||||||
@ -220,6 +222,7 @@ local function tabulateCommon(bPaged, ...)
|
|||||||
term.setTextColor(t)
|
term.setTextColor(t)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
term.setTextColor(previous_colour)
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[- Prints tables in a structured form.
|
--[[- Prints tables in a structured form.
|
||||||
@ -685,6 +688,7 @@ do
|
|||||||
@treturn[2] nil If the object could not be deserialised.
|
@treturn[2] nil If the object could not be deserialised.
|
||||||
@treturn string A message describing why the JSON string is invalid.
|
@treturn string A message describing why the JSON string is invalid.
|
||||||
@since 1.87.0
|
@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.json_null Use to serialize a JSON `null` value.
|
||||||
@see textutils.empty_json_array Use to serialize a JSON empty array.
|
@see textutils.empty_json_array Use to serialize a JSON empty array.
|
||||||
@usage Unserialise a basic JSON object
|
@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.
|
@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
|
@usage Reads "data/example.dfpwm" in chunks, decodes them and then doubles the speed of the audio. The resulting audio
|
||||||
is then re-encoded and saved to "speedy.dfpwm". This processed audio can then be played with the `speaker` program.
|
is then re-encoded and saved to "speedy.dfpwm". This processed audio can then be played with the `speaker` program.
|
||||||
|
@since 1.100.0
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local dfpwm = require("cc.audio.dfpwm")
|
local dfpwm = require("cc.audio.dfpwm")
|
||||||
|
@ -331,9 +331,8 @@ while #tProcesses > 0 do
|
|||||||
resizeWindows()
|
resizeWindows()
|
||||||
redrawMenu()
|
redrawMenu()
|
||||||
|
|
||||||
elseif sEvent == "char" or sEvent == "key" or sEvent == "key_up" or sEvent == "paste" or sEvent == "terminate" then
|
elseif sEvent == "char" or sEvent == "key" or sEvent == "key_up" or sEvent == "paste" or sEvent == "terminate" or sEvent == "file_transfer" then
|
||||||
-- Keyboard event
|
-- Basic input, just passthrough to current process
|
||||||
-- Passthrough to current process
|
|
||||||
resumeProcess(nCurrentProcess, table.unpack(tEventData, 1, tEventData.n))
|
resumeProcess(nCurrentProcess, table.unpack(tEventData, 1, tEventData.n))
|
||||||
if cullProcess(nCurrentProcess) then
|
if cullProcess(nCurrentProcess) then
|
||||||
setMenuVisible(#tProcesses >= 2)
|
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
|
-- @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 make_package = dofile("rom/modules/main/cc/require.lua").make
|
||||||
|
|
||||||
local multishell = multishell
|
local multishell = multishell
|
||||||
@ -35,6 +34,14 @@ local function createShellEnv(dir)
|
|||||||
return env
|
return env
|
||||||
end
|
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
|
-- Colours
|
||||||
local promptColour, textColour, bgColour
|
local promptColour, textColour, bgColour
|
||||||
if term.isColour() then
|
if term.isColour() then
|
||||||
@ -591,6 +598,13 @@ if #tArgs > 0 then
|
|||||||
shell.run(...)
|
shell.run(...)
|
||||||
|
|
||||||
else
|
else
|
||||||
|
local function show_prompt()
|
||||||
|
term.setBackgroundColor(bgColour)
|
||||||
|
term.setTextColour(promptColour)
|
||||||
|
write(shell.dir() .. "> ")
|
||||||
|
term.setTextColour(textColour)
|
||||||
|
end
|
||||||
|
|
||||||
-- "shell"
|
-- "shell"
|
||||||
-- Print the header
|
-- Print the header
|
||||||
term.setBackgroundColor(bgColour)
|
term.setBackgroundColor(bgColour)
|
||||||
@ -607,21 +621,49 @@ else
|
|||||||
local tCommandHistory = {}
|
local tCommandHistory = {}
|
||||||
while not bExit do
|
while not bExit do
|
||||||
term.redirect(parentTerm)
|
term.redirect(parentTerm)
|
||||||
term.setBackgroundColor(bgColour)
|
show_prompt()
|
||||||
term.setTextColour(promptColour)
|
|
||||||
write(shell.dir() .. "> ")
|
|
||||||
term.setTextColour(textColour)
|
|
||||||
|
|
||||||
|
|
||||||
local sLine
|
local complete
|
||||||
if settings.get("shell.autocomplete") then
|
if settings.get("shell.autocomplete") then complete = shell.complete end
|
||||||
sLine = read(nil, tCommandHistory, shell.complete)
|
|
||||||
else
|
local ok, result
|
||||||
sLine = read(nil, tCommandHistory)
|
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
|
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
|
end
|
||||||
shell.run(sLine)
|
shell.run(result)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -11,18 +11,14 @@ import dan200.computercraft.api.lua.ILuaAPI;
|
|||||||
import dan200.computercraft.api.lua.LuaException;
|
import dan200.computercraft.api.lua.LuaException;
|
||||||
import dan200.computercraft.api.lua.LuaFunction;
|
import dan200.computercraft.api.lua.LuaFunction;
|
||||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||||
import dan200.computercraft.core.computer.BasicEnvironment;
|
|
||||||
import dan200.computercraft.core.computer.Computer;
|
import dan200.computercraft.core.computer.Computer;
|
||||||
import dan200.computercraft.core.computer.ComputerSide;
|
import dan200.computercraft.core.computer.ComputerSide;
|
||||||
import dan200.computercraft.core.computer.FakeMainThreadScheduler;
|
|
||||||
import dan200.computercraft.core.filesystem.FileMount;
|
import dan200.computercraft.core.filesystem.FileMount;
|
||||||
import dan200.computercraft.core.filesystem.FileSystemException;
|
import dan200.computercraft.core.filesystem.FileSystemException;
|
||||||
import dan200.computercraft.core.terminal.Terminal;
|
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 dan200.computercraft.support.TestFiles;
|
||||||
import net.minecraft.world.level.Level;
|
import dan200.computercraft.test.core.computer.BasicEnvironment;
|
||||||
import net.minecraft.world.phys.Vec3;
|
import dan200.computercraft.test.core.computer.FakeMainThreadScheduler;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.junit.jupiter.api.*;
|
import org.junit.jupiter.api.*;
|
||||||
@ -32,8 +28,10 @@ import org.opentest4j.AssertionFailedError;
|
|||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.io.BufferedWriter;
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
|
import java.net.URI;
|
||||||
import java.nio.channels.Channels;
|
import java.nio.channels.Channels;
|
||||||
import java.nio.channels.WritableByteChannel;
|
import java.nio.channels.WritableByteChannel;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@ -47,8 +45,6 @@ import java.util.regex.Matcher;
|
|||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static dan200.computercraft.api.lua.LuaValues.getType;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads tests from {@code test-rom/spec} and executes them.
|
* Loads tests from {@code test-rom/spec} and executes them.
|
||||||
* <p>
|
* <p>
|
||||||
@ -118,6 +114,7 @@ public class ComputerTestDelegate
|
|||||||
context = new ComputerContext( environment, 1, new FakeMainThreadScheduler() );
|
context = new ComputerContext( environment, 1, new FakeMainThreadScheduler() );
|
||||||
computer = new Computer( context, environment, term, 0 );
|
computer = new Computer( context, environment, term, 0 );
|
||||||
computer.getEnvironment().setPeripheral( ComputerSide.TOP, new FakeModem() );
|
computer.getEnvironment().setPeripheral( ComputerSide.TOP, new FakeModem() );
|
||||||
|
computer.getEnvironment().setPeripheral( ComputerSide.BOTTOM, new FakePeripheralHub() );
|
||||||
computer.addApi( new CctTestAPI() );
|
computer.addApi( new CctTestAPI() );
|
||||||
|
|
||||||
computer.turnOn();
|
computer.turnOn();
|
||||||
@ -198,36 +195,45 @@ public class ComputerTestDelegate
|
|||||||
private static class DynamicNodeBuilder
|
private static class DynamicNodeBuilder
|
||||||
{
|
{
|
||||||
private final String name;
|
private final String name;
|
||||||
|
private final URI uri;
|
||||||
private final Map<String, DynamicNodeBuilder> children;
|
private final Map<String, DynamicNodeBuilder> children;
|
||||||
private final Executable executor;
|
private final Executable executor;
|
||||||
|
|
||||||
DynamicNodeBuilder( String name )
|
DynamicNodeBuilder( String name, String path )
|
||||||
{
|
{
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
this.uri = getUri( path );
|
||||||
this.children = new HashMap<>();
|
this.children = new HashMap<>();
|
||||||
this.executor = null;
|
this.executor = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
DynamicNodeBuilder( String name, Executable executor )
|
DynamicNodeBuilder( String name, String path, Executable executor )
|
||||||
{
|
{
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
this.uri = getUri( path );
|
||||||
this.children = Collections.emptyMap();
|
this.children = Collections.emptyMap();
|
||||||
this.executor = executor;
|
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 get( String name )
|
||||||
{
|
{
|
||||||
DynamicNodeBuilder child = children.get( 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;
|
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( this.executor != null ) throw new IllegalStateException( name + " is leaf node" );
|
||||||
if( children.containsKey( name ) ) throw new IllegalStateException( "Duplicate key for " + name );
|
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()
|
boolean isActive()
|
||||||
@ -244,8 +250,8 @@ public class ComputerTestDelegate
|
|||||||
DynamicNode build()
|
DynamicNode build()
|
||||||
{
|
{
|
||||||
return executor == null
|
return executor == null
|
||||||
? DynamicContainer.dynamicContainer( name, buildChildren() )
|
? DynamicContainer.dynamicContainer( name, uri, buildChildren() )
|
||||||
: DynamicTest.dynamicTest( name, executor );
|
: DynamicTest.dynamicTest( name, uri, executor );
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<DynamicNode> buildChildren()
|
Stream<DynamicNode> buildChildren()
|
||||||
@ -282,26 +288,13 @@ public class ComputerTestDelegate
|
|||||||
return name.replace( "\0", " -> " );
|
return name.replace( "\0", " -> " );
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class FakeModem extends WirelessModemPeripheral
|
public static class FakeModem implements IPeripheral
|
||||||
{
|
{
|
||||||
FakeModem()
|
|
||||||
{
|
|
||||||
super( new ModemState(), true );
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings( "ConstantConditions" )
|
public String getType()
|
||||||
public Level getLevel()
|
|
||||||
{
|
{
|
||||||
return null;
|
return "modem";
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public Vec3 getPosition()
|
|
||||||
{
|
|
||||||
return Vec3.ZERO;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -309,6 +302,58 @@ public class ComputerTestDelegate
|
|||||||
{
|
{
|
||||||
return this == other;
|
return this == other;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@LuaFunction
|
||||||
|
public final boolean isOpen( int channel )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FakePeripheralHub implements IPeripheral
|
||||||
|
{
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getType()
|
||||||
|
{
|
||||||
|
return "peripheral_hub";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals( @Nullable IPeripheral other )
|
||||||
|
{
|
||||||
|
return this == other;
|
||||||
|
}
|
||||||
|
|
||||||
|
@LuaFunction
|
||||||
|
public final Collection<String> getNamesRemote()
|
||||||
|
{
|
||||||
|
return Collections.singleton( "remote_1" );
|
||||||
|
}
|
||||||
|
|
||||||
|
@LuaFunction
|
||||||
|
public final boolean isPresentRemote( String name )
|
||||||
|
{
|
||||||
|
return name.equals( "remote_1" );
|
||||||
|
}
|
||||||
|
|
||||||
|
@LuaFunction
|
||||||
|
public final Object[] getTypeRemote( String name )
|
||||||
|
{
|
||||||
|
return name.equals( "remote_1" ) ? new Object[] { "remote", "other_type" } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@LuaFunction
|
||||||
|
public final Object[] hasTypeRemote( String name, String type )
|
||||||
|
{
|
||||||
|
return name.equals( "remote_1" ) ? new Object[] { type.equals( "remote" ) || type.equals( "other_type" ) } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@LuaFunction
|
||||||
|
public final Object[] getMethodsRemote( String name )
|
||||||
|
{
|
||||||
|
return name.equals( "remote_1" ) ? new Object[] { Collections.singletonList( "func" ) } : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CctTestAPI implements ILuaAPI
|
public class CctTestAPI implements ILuaAPI
|
||||||
@ -340,15 +385,17 @@ public class ComputerTestDelegate
|
|||||||
{
|
{
|
||||||
// Submit several tests and signal for #get to run
|
// Submit several tests and signal for #get to run
|
||||||
LOG.info( "Received tests from computer" );
|
LOG.info( "Received tests from computer" );
|
||||||
DynamicNodeBuilder root = new DynamicNodeBuilder( "" );
|
DynamicNodeBuilder root = new DynamicNodeBuilder( "", null );
|
||||||
for( Object key : tests.keySet() )
|
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" );
|
String[] parts = name.split( "\0" );
|
||||||
DynamicNodeBuilder builder = root;
|
DynamicNodeBuilder builder = root;
|
||||||
for( int i = 0; i < parts.length - 1; i++ ) builder = builder.get( parts[i] );
|
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
|
// Run it
|
||||||
lock.lockInterruptibly();
|
lock.lockInterruptibly();
|
||||||
try
|
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.Map;
|
||||||
import java.util.Optional;
|
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.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.*;
|
import static org.hamcrest.Matchers.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
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.api.lua.LuaFunction;
|
||||||
import dan200.computercraft.core.ComputerContext;
|
import dan200.computercraft.core.ComputerContext;
|
||||||
import dan200.computercraft.core.computer.mainthread.MainThread;
|
import dan200.computercraft.core.computer.mainthread.MainThread;
|
||||||
import dan200.computercraft.core.filesystem.MemoryMount;
|
|
||||||
import dan200.computercraft.core.terminal.Terminal;
|
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 org.junit.jupiter.api.Assertions;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
@ -8,6 +8,7 @@ package dan200.computercraft.core.computer;
|
|||||||
import dan200.computercraft.ComputerCraft;
|
import dan200.computercraft.ComputerCraft;
|
||||||
import dan200.computercraft.core.lua.MachineResult;
|
import dan200.computercraft.core.lua.MachineResult;
|
||||||
import dan200.computercraft.support.ConcurrentHelpers;
|
import dan200.computercraft.support.ConcurrentHelpers;
|
||||||
|
import dan200.computercraft.test.core.computer.KotlinComputerManager;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@ -25,12 +26,12 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
@Execution( ExecutionMode.CONCURRENT )
|
@Execution( ExecutionMode.CONCURRENT )
|
||||||
public class ComputerThreadTest
|
public class ComputerThreadTest
|
||||||
{
|
{
|
||||||
private FakeComputerManager manager;
|
private KotlinComputerManager manager;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void before()
|
public void before()
|
||||||
{
|
{
|
||||||
manager = new FakeComputerManager();
|
manager = new KotlinComputerManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@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.api.lua.LuaValues;
|
||||||
import dan200.computercraft.shared.util.Colour;
|
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 io.netty.buffer.Unpooled;
|
||||||
import net.minecraft.nbt.CompoundTag;
|
import net.minecraft.nbt.CompoundTag;
|
||||||
import net.minecraft.network.FriendlyByteBuf;
|
import net.minecraft.network.FriendlyByteBuf;
|
||||||
@ -16,7 +17,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
|
|
||||||
import java.nio.ByteBuffer;
|
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.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.allOf;
|
import static org.hamcrest.Matchers.allOf;
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
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.FileSlice;
|
||||||
import dan200.computercraft.shared.computer.upload.FileUpload;
|
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.FakeContainer;
|
||||||
import dan200.computercraft.support.WithMinecraft;
|
import dan200.computercraft.support.WithMinecraft;
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
@ -22,9 +22,9 @@ import java.util.List;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static dan200.computercraft.shared.network.server.UploadFileMessage.*;
|
import static dan200.computercraft.shared.network.server.UploadFileMessage.*;
|
||||||
import static dan200.computercraft.support.ByteBufferMatcher.bufferEqual;
|
import static dan200.computercraft.test.core.ByteBufferMatcher.bufferEqual;
|
||||||
import static dan200.computercraft.support.ContramapMatcher.contramap;
|
import static dan200.computercraft.test.core.ContramapMatcher.contramap;
|
||||||
import static dan200.computercraft.support.CustomMatchers.containsWith;
|
import static dan200.computercraft.test.core.CustomMatchers.containsWith;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.*;
|
import static org.hamcrest.Matchers.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
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.ComputerCraft
|
||||||
import dan200.computercraft.core.apis.AsyncRunner
|
|
||||||
import dan200.computercraft.core.apis.HTTPAPI
|
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.AfterAll
|
||||||
import org.junit.jupiter.api.Assertions.assertArrayEquals
|
import org.junit.jupiter.api.Assertions.assertArrayEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
@ -36,15 +38,15 @@ class TestHttpApi {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Connects to websocket`() {
|
fun `Connects to websocket`() {
|
||||||
AsyncRunner.runTest {
|
LuaTaskRunner.runTest {
|
||||||
val httpApi = addApi(HTTPAPI(this))
|
val httpApi = addApi(HTTPAPI(environment))
|
||||||
|
|
||||||
val result = httpApi.websocket(WS_ADDRESS, Optional.empty())
|
val result = httpApi.websocket(WS_ADDRESS, Optional.empty())
|
||||||
assertArrayEquals(arrayOf(true), result, "Should have created websocket")
|
assertArrayEquals(arrayOf(true), result, "Should have created websocket")
|
||||||
|
|
||||||
val event = pullEvent()
|
val event = pullEvent()
|
||||||
assertEquals("websocket_success", event.name) {
|
assertEquals("websocket_success", event[0]) {
|
||||||
"Websocket failed to connect: ${event.args.contentToString()}"
|
"Websocket failed to connect: ${event.contentToString()}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -424,6 +424,8 @@ local tests_locked = false
|
|||||||
local test_list = {}
|
local test_list = {}
|
||||||
local test_map, test_count = {}, 0
|
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.
|
--- Add a new test to our queue.
|
||||||
--
|
--
|
||||||
-- @param test The descriptor of this test
|
-- @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
|
if not test.name then test.name = table.concat(test_stack, "\0", 1, test_stack.n) end
|
||||||
test_count = test_count + 1
|
test_count = test_count + 1
|
||||||
test_list[test_count] = test
|
test_list[test_count] = test
|
||||||
test_map[test.name] = test_count
|
test_map[test.name] = { idx = test_count, definition = test.definition }
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Get the "friendly" name of this test.
|
--- Get the "friendly" name of this test.
|
||||||
@ -456,7 +458,7 @@ local function describe(name, body)
|
|||||||
local ok, err = try(body)
|
local ok, err = try(body)
|
||||||
|
|
||||||
-- We count errors as a (failing) test.
|
-- 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
|
test_stack.n = n - 1
|
||||||
end
|
end
|
||||||
@ -475,7 +477,7 @@ local function it(name, body)
|
|||||||
local n = test_stack.n + 1
|
local n = test_stack.n + 1
|
||||||
test_stack[n], test_stack.n, tests_locked = name, n, true
|
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
|
-- Pop the test from the stack
|
||||||
test_stack.n, tests_locked = n - 1, false
|
test_stack.n, tests_locked = n - 1, false
|
||||||
@ -488,12 +490,11 @@ local function pending(name)
|
|||||||
check('it', 1, 'string', name)
|
check('it', 1, 'string', name)
|
||||||
if tests_locked then error("Cannot create test while running tests", 2) end
|
if tests_locked then error("Cannot create test while running tests", 2) end
|
||||||
|
|
||||||
local _, loc = pcall(error, "", 3)
|
local trace = format_loc(debug.getinfo(2, "Sl"))
|
||||||
loc = loc:gsub(":%s*$", "")
|
|
||||||
|
|
||||||
local n = test_stack.n + 1
|
local n = test_stack.n + 1
|
||||||
test_stack[n], test_stack.n = name, n
|
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
|
test_stack.n = n - 1
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -667,7 +668,7 @@ if cct_test then
|
|||||||
while true do
|
while true do
|
||||||
local _, name = os.pullEvent("cct_test_run")
|
local _, name = os.pullEvent("cct_test_run")
|
||||||
if not name then break end
|
if not name then break end
|
||||||
do_run(test_list[test_map[name]])
|
do_run(test_list[test_map[name].idx])
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
for _, test in pairs(test_list) do do_run(test) end
|
for _, test in pairs(test_list) do do_run(test) end
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
describe("The peripheral library", function()
|
describe("The peripheral library", function()
|
||||||
local it_modem = peripheral.getType("top") == "modem" and it or pending
|
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({}, {
|
local multitype_peripheral = setmetatable({}, {
|
||||||
__name = "peripheral",
|
__name = "peripheral",
|
||||||
@ -13,6 +14,16 @@ describe("The peripheral library", function()
|
|||||||
peripheral.isPresent("")
|
peripheral.isPresent("")
|
||||||
expect.error(peripheral.isPresent, nil):eq("bad argument #1 (expected string, got nil)")
|
expect.error(peripheral.isPresent, nil):eq("bad argument #1 (expected string, got nil)")
|
||||||
end)
|
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)
|
end)
|
||||||
|
|
||||||
describe("peripheral.getName", function()
|
describe("peripheral.getName", function()
|
||||||
@ -24,6 +35,10 @@ describe("The peripheral library", function()
|
|||||||
it_modem("can get the name of a wrapped peripheral", function()
|
it_modem("can get the name of a wrapped peripheral", function()
|
||||||
expect(peripheral.getName(peripheral.wrap("top"))):eq("top")
|
expect(peripheral.getName(peripheral.wrap("top"))):eq("top")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it("can get the name of a fake peripheral", function()
|
||||||
|
expect(peripheral.getName(multitype_peripheral)):eq("top")
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe("peripheral.getType", function()
|
describe("peripheral.getType", function()
|
||||||
@ -34,13 +49,18 @@ describe("The peripheral library", function()
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
it("returns nil when no peripheral is present", function()
|
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)
|
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")
|
expect(peripheral.getType("top")):eq("modem")
|
||||||
end)
|
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()
|
it_modem("can get the type of a wrapped peripheral", function()
|
||||||
expect(peripheral.getType(peripheral.wrap("top"))):eq("modem")
|
expect(peripheral.getType(peripheral.wrap("top"))):eq("modem")
|
||||||
end)
|
end)
|
||||||
@ -59,7 +79,8 @@ describe("The peripheral library", function()
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
it("returns nil when no peripherals are present", function()
|
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)
|
end)
|
||||||
|
|
||||||
it_modem("can check type of a peripheral by side", function()
|
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, "inventory")):eq(true)
|
||||||
expect(peripheral.hasType(multitype_peripheral, "something else")):eq(false)
|
expect(peripheral.hasType(multitype_peripheral, "something else")):eq(false)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it_remote("can check type of a remote peripheral", function()
|
||||||
|
expect(peripheral.hasType("remote_1", "remote")):eq(true)
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe("peripheral.getMethods", function()
|
describe("peripheral.getMethods", function()
|
||||||
@ -103,6 +128,18 @@ describe("The peripheral library", function()
|
|||||||
peripheral.wrap("")
|
peripheral.wrap("")
|
||||||
expect.error(peripheral.wrap, nil):eq("bad argument #1 (expected string, got nil)")
|
expect.error(peripheral.wrap, nil):eq("bad argument #1 (expected string, got nil)")
|
||||||
end)
|
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)
|
end)
|
||||||
|
|
||||||
describe("peripheral.find", function()
|
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, nil):eq("bad argument #1 (expected string, got nil)")
|
||||||
expect.error(peripheral.find, "", false):eq("bad argument #2 (expected function, got boolean)")
|
expect.error(peripheral.find, "", false):eq("bad argument #2 (expected function, got boolean)")
|
||||||
end)
|
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)
|
||||||
end)
|
end)
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
local with_window = require "test_helpers".with_window
|
||||||
|
|
||||||
describe("The shell", function()
|
describe("The shell", function()
|
||||||
describe("require", function()
|
describe("require", function()
|
||||||
it("validates arguments", 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)")
|
expect.error(shell.switchTab, nil):eq("bad argument #1 (expected number, got nil)")
|
||||||
end)
|
end)
|
||||||
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)
|
end)
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
*/
|
*/
|
||||||
package dan200.computercraft.support;
|
package dan200.computercraft.test.core;
|
||||||
|
|
||||||
import net.jqwik.api.*;
|
import net.jqwik.api.*;
|
||||||
import net.jqwik.api.arbitraries.SizableArbitrary;
|
import net.jqwik.api.arbitraries.SizableArbitrary;
|
@ -3,7 +3,7 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
*/
|
*/
|
||||||
package dan200.computercraft.support;
|
package dan200.computercraft.test.core;
|
||||||
|
|
||||||
import org.hamcrest.Description;
|
import org.hamcrest.Description;
|
||||||
import org.hamcrest.Matcher;
|
import org.hamcrest.Matcher;
|
@ -3,7 +3,7 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
*/
|
*/
|
||||||
package dan200.computercraft.support;
|
package dan200.computercraft.test.core;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
@ -3,7 +3,7 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
*/
|
*/
|
||||||
package dan200.computercraft.support;
|
package dan200.computercraft.test.core;
|
||||||
|
|
||||||
import org.hamcrest.FeatureMatcher;
|
import org.hamcrest.FeatureMatcher;
|
||||||
import org.hamcrest.Matcher;
|
import org.hamcrest.Matcher;
|
@ -3,7 +3,7 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
*/
|
*/
|
||||||
package dan200.computercraft.support;
|
package dan200.computercraft.test.core;
|
||||||
|
|
||||||
import org.hamcrest.Matcher;
|
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.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
*/
|
*/
|
||||||
package dan200.computercraft.core.computer;
|
package dan200.computercraft.test.core.computer;
|
||||||
|
|
||||||
import dan200.computercraft.ComputerCraft;
|
import dan200.computercraft.ComputerCraft;
|
||||||
import dan200.computercraft.api.filesystem.IMount;
|
import dan200.computercraft.api.filesystem.IMount;
|
||||||
import dan200.computercraft.api.filesystem.IWritableMount;
|
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.FileMount;
|
||||||
import dan200.computercraft.core.filesystem.JarMount;
|
import dan200.computercraft.core.filesystem.JarMount;
|
||||||
import dan200.computercraft.core.filesystem.MemoryMount;
|
|
||||||
import dan200.computercraft.core.metrics.Metric;
|
import dan200.computercraft.core.metrics.Metric;
|
||||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||||
|
import dan200.computercraft.test.core.filesystem.MemoryMount;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -24,7 +26,8 @@ import java.net.URISyntaxException;
|
|||||||
import java.net.URL;
|
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
|
public class BasicEnvironment implements ComputerEnvironment, GlobalEnvironment, MetricsObserver
|
||||||
{
|
{
|
@ -3,7 +3,7 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
*/
|
*/
|
||||||
package dan200.computercraft.core.computer;
|
package dan200.computercraft.test.core.computer;
|
||||||
|
|
||||||
import dan200.computercraft.core.computer.mainthread.MainThreadScheduler;
|
import dan200.computercraft.core.computer.mainthread.MainThreadScheduler;
|
||||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
import dan200.computercraft.core.metrics.MetricsObserver;
|
@ -3,7 +3,7 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
*/
|
*/
|
||||||
package dan200.computercraft.core.filesystem;
|
package dan200.computercraft.test.core.filesystem;
|
||||||
|
|
||||||
import dan200.computercraft.api.filesystem.IWritableMount;
|
import dan200.computercraft.api.filesystem.IWritableMount;
|
||||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
@ -3,9 +3,11 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
*/
|
*/
|
||||||
package dan200.computercraft.core.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.Matcher;
|
||||||
import org.hamcrest.Matchers;
|
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…
x
Reference in New Issue
Block a user