mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-10-31 05:33:00 +00:00 
			
		
		
		
	Merge branch 'mc-1.16.x' into mc-1.18.x
This commit is contained in:
		| @@ -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) } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Jonathan Coates
					Jonathan Coates