mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-11-04 07:32:59 +00:00 
			
		
		
		
	Compare commits
	
		
			35 Commits
		
	
	
		
			v1.20.1-1.
			...
			v1.20.1-1.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					a0f759527d | ||
| 
						 | 
					385e4210fa | ||
| 
						 | 
					d2896473f2 | ||
| 
						 | 
					f14cb2a3d1 | ||
| 
						 | 
					8db5c6bc3a | ||
| 
						 | 
					f26e443e81 | ||
| 
						 | 
					033378333f | ||
| 
						 | 
					ebeaa757a9 | ||
| 
						 | 
					57b1a65db3 | ||
| 
						 | 
					27c72a4571 | ||
| 
						 | 
					f284328656 | ||
| 
						 | 
					6b83c63991 | ||
| 
						 | 
					b27526bd21 | ||
| 
						 | 
					cb25f6c08a | ||
| 
						 | 
					d38b1d04e7 | ||
| 
						 | 
					9ccee75a99 | ||
| 
						 | 
					359c8d6652 | ||
| 
						 | 
					1788afacfc | ||
| 
						 | 
					f695f22d8a | ||
| 
						 | 
					bc03090ca4 | ||
| 
						 | 
					a617d0d566 | ||
| 
						 | 
					36599b321e | ||
| 
						 | 
					1d6e3f4fc0 | ||
| 
						 | 
					30dc4cb38c | ||
| 
						 | 
					be4512d1c3 | ||
| 
						 | 
					e6ee292850 | ||
| 
						 | 
					9d36f72bad | ||
| 
						 | 
					b5923c4462 | ||
| 
						 | 
					4d1e689719 | ||
| 
						 | 
					9d4af07568 | ||
| 
						 | 
					89294f4a22 | ||
| 
						 | 
					133b51b092 | ||
| 
						 | 
					272010e945 | ||
| 
						 | 
					e0889c613a | ||
| 
						 | 
					f115d43d07 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -7,6 +7,7 @@
 | 
			
		||||
/logs
 | 
			
		||||
/build
 | 
			
		||||
/projects/*/logs
 | 
			
		||||
/projects/fabric/fabricloader.log
 | 
			
		||||
/projects/*/build
 | 
			
		||||
/buildSrc/build
 | 
			
		||||
/out
 | 
			
		||||
 
 | 
			
		||||
@@ -75,8 +75,8 @@ minecraft {
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
You should also be careful to only use classes within the `dan200.computercraft.api` package. Non-API classes are
 | 
			
		||||
subject to change at any point. If you depend on functionality outside the API, file an issue, and we can look into
 | 
			
		||||
exposing more features.
 | 
			
		||||
subject to change at any point. If you depend on functionality outside the API (or need to mixin to CC:T), please file
 | 
			
		||||
an issue to let me know!
 | 
			
		||||
 | 
			
		||||
We bundle the API sources with the jar, so documentation should be easily viewable within your editor. Alternatively,
 | 
			
		||||
the generated documentation [can be browsed online](https://tweaked.cc/javadoc/).
 | 
			
		||||
 
 | 
			
		||||
@@ -29,19 +29,19 @@ repositories {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    maven("https://repo.spongepowered.org/repository/maven-public/") {
 | 
			
		||||
        name = "Sponge"
 | 
			
		||||
        content {
 | 
			
		||||
            includeGroup("org.spongepowered")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    maven("https://maven.fabricmc.net/") {
 | 
			
		||||
        name = "Fabric"
 | 
			
		||||
        content {
 | 
			
		||||
            includeGroup("net.fabricmc")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    maven("https://squiddev.cc/maven") {
 | 
			
		||||
        name = "SquidDev"
 | 
			
		||||
        content {
 | 
			
		||||
            includeGroup("cc.tweaked.vanilla-extract")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
dependencies {
 | 
			
		||||
@@ -55,8 +55,7 @@ dependencies {
 | 
			
		||||
    implementation(libs.ideaExt)
 | 
			
		||||
    implementation(libs.librarian)
 | 
			
		||||
    implementation(libs.minotaur)
 | 
			
		||||
    implementation(libs.vanillaGradle)
 | 
			
		||||
    implementation(libs.vineflower)
 | 
			
		||||
    implementation(libs.vanillaExtract)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
gradlePlugin {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,6 @@ import cc.tweaked.gradle.MinecraftConfigurations
 | 
			
		||||
plugins {
 | 
			
		||||
    `java-library`
 | 
			
		||||
    id("fabric-loom")
 | 
			
		||||
    id("io.github.juuxel.loom-vineflower")
 | 
			
		||||
    id("cc-tweaked.java-convention")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,25 +10,31 @@ import cc.tweaked.gradle.MinecraftConfigurations
 | 
			
		||||
 | 
			
		||||
plugins {
 | 
			
		||||
    id("cc-tweaked.java-convention")
 | 
			
		||||
    id("org.spongepowered.gradle.vanilla")
 | 
			
		||||
    id("cc.tweaked.vanilla-extract")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
plugins.apply(CCTweakedPlugin::class.java)
 | 
			
		||||
 | 
			
		||||
val mcVersion: String by extra
 | 
			
		||||
 | 
			
		||||
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
 | 
			
		||||
 | 
			
		||||
minecraft {
 | 
			
		||||
    version(mcVersion)
 | 
			
		||||
 | 
			
		||||
    mappings {
 | 
			
		||||
        parchment(libs.findVersion("parchmentMc").get().toString(), libs.findVersion("parchment").get().toString())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    unpick(libs.findLibrary("yarn").get())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
dependencies {
 | 
			
		||||
    val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
 | 
			
		||||
 | 
			
		||||
    // Depend on error prone annotations to silence a lot of compile warnings.
 | 
			
		||||
    compileOnlyApi(libs.findLibrary("errorProne.annotations").get())
 | 
			
		||||
    compileOnly(libs.findLibrary("errorProne.annotations").get())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
MinecraftConfigurations.setup(project)
 | 
			
		||||
MinecraftConfigurations.setupBasic(project)
 | 
			
		||||
 | 
			
		||||
extensions.configure(CCTweakedExtension::class.java) {
 | 
			
		||||
    linters(minecraft = true, loader = null)
 | 
			
		||||
 
 | 
			
		||||
@@ -255,7 +255,7 @@ abstract class CCTweakedExtension(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Exclude a dependency from being publisehd in Maven.
 | 
			
		||||
     * Exclude a dependency from being published in Maven.
 | 
			
		||||
     */
 | 
			
		||||
    fun exclude(dep: Dependency) {
 | 
			
		||||
        excludedDeps.add(dep)
 | 
			
		||||
 
 | 
			
		||||
@@ -7,12 +7,15 @@ package cc.tweaked.gradle
 | 
			
		||||
import org.gradle.api.DefaultTask
 | 
			
		||||
import org.gradle.api.GradleException
 | 
			
		||||
import org.gradle.api.artifacts.Configuration
 | 
			
		||||
import org.gradle.api.artifacts.MinimalExternalModuleDependency
 | 
			
		||||
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
 | 
			
		||||
import org.gradle.api.artifacts.component.ModuleComponentSelector
 | 
			
		||||
import org.gradle.api.artifacts.component.ProjectComponentIdentifier
 | 
			
		||||
import org.gradle.api.artifacts.result.DependencyResult
 | 
			
		||||
import org.gradle.api.artifacts.result.ResolvedDependencyResult
 | 
			
		||||
import org.gradle.api.provider.ListProperty
 | 
			
		||||
import org.gradle.api.provider.MapProperty
 | 
			
		||||
import org.gradle.api.provider.Provider
 | 
			
		||||
import org.gradle.api.tasks.Input
 | 
			
		||||
import org.gradle.api.tasks.TaskAction
 | 
			
		||||
import org.gradle.language.base.plugins.LifecycleBasePlugin
 | 
			
		||||
@@ -21,9 +24,25 @@ abstract class DependencyCheck : DefaultTask() {
 | 
			
		||||
    @get:Input
 | 
			
		||||
    abstract val configuration: ListProperty<Configuration>
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A mapping of module coordinates (`group:module`) to versions, overriding the requested version.
 | 
			
		||||
     */
 | 
			
		||||
    @get:Input
 | 
			
		||||
    abstract val overrides: MapProperty<String, String>
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        description = "Check :core's dependencies are consistent with Minecraft's."
 | 
			
		||||
        group = LifecycleBasePlugin.VERIFICATION_GROUP
 | 
			
		||||
 | 
			
		||||
        configuration.finalizeValueOnRead()
 | 
			
		||||
        overrides.finalizeValueOnRead()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Override a module with a different version.
 | 
			
		||||
     */
 | 
			
		||||
    fun override(module: Provider<MinimalExternalModuleDependency>, version: String) {
 | 
			
		||||
        overrides.putAll(project.provider { mutableMapOf(module.get().module.toString() to version) })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @TaskAction
 | 
			
		||||
@@ -60,7 +79,8 @@ abstract class DependencyCheck : DefaultTask() {
 | 
			
		||||
        ) {
 | 
			
		||||
            // If the version is different between the requested and selected version, report an error.
 | 
			
		||||
            val selected = dependency.selected.moduleVersion!!.version
 | 
			
		||||
            if (requested.version != selected) {
 | 
			
		||||
            val requestedVersion = overrides.get()["${requested.group}:${requested.module}"] ?: requested.version
 | 
			
		||||
            if (requestedVersion != selected) {
 | 
			
		||||
                logger.error("Requested dependency {} (via {}) but got version {}", requested, from, selected)
 | 
			
		||||
                return false
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,23 +4,17 @@
 | 
			
		||||
 | 
			
		||||
package cc.tweaked.gradle
 | 
			
		||||
 | 
			
		||||
import cc.tweaked.vanillaextract.configurations.Capabilities
 | 
			
		||||
import cc.tweaked.vanillaextract.configurations.MinecraftSetup
 | 
			
		||||
import org.gradle.api.Project
 | 
			
		||||
import org.gradle.api.artifacts.Configuration
 | 
			
		||||
import org.gradle.api.artifacts.ModuleDependency
 | 
			
		||||
import org.gradle.api.artifacts.dsl.DependencyHandler
 | 
			
		||||
import org.gradle.api.attributes.Bundling
 | 
			
		||||
import org.gradle.api.attributes.Category
 | 
			
		||||
import org.gradle.api.attributes.LibraryElements
 | 
			
		||||
import org.gradle.api.attributes.Usage
 | 
			
		||||
import org.gradle.api.attributes.java.TargetJvmVersion
 | 
			
		||||
import org.gradle.api.capabilities.Capability
 | 
			
		||||
import org.gradle.api.plugins.BasePlugin
 | 
			
		||||
import org.gradle.api.plugins.JavaPluginExtension
 | 
			
		||||
import org.gradle.api.tasks.SourceSet
 | 
			
		||||
import org.gradle.api.tasks.bundling.Jar
 | 
			
		||||
import org.gradle.api.tasks.javadoc.Javadoc
 | 
			
		||||
import org.gradle.kotlin.dsl.get
 | 
			
		||||
import org.gradle.kotlin.dsl.named
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This sets up a separate client-only source set, and extends that and the main/common source set with additional
 | 
			
		||||
@@ -59,31 +53,13 @@ class MinecraftConfigurations private constructor(private val project: Project)
 | 
			
		||||
        }
 | 
			
		||||
        configurations.named(client.implementationConfigurationName) { extendsFrom(clientApi) }
 | 
			
		||||
 | 
			
		||||
        /*
 | 
			
		||||
          Now add outgoing variants for the main and common source sets that we can consume downstream. This is possibly
 | 
			
		||||
          the worst way to do things, but unfortunately the alternatives don't actually work very well:
 | 
			
		||||
 | 
			
		||||
           - Just using source set outputs: This means dependencies don't propagate, which means when :fabric depends
 | 
			
		||||
             on :fabric-api, we don't inherit the fake :common-api in IDEA.
 | 
			
		||||
 | 
			
		||||
           - Having separate common/main jars: Nice in principle, but unfortunately Forge needs a separate deobf jar
 | 
			
		||||
             task (as the original jar is obfuscated), and IDEA is not able to map its output back to a source set.
 | 
			
		||||
 | 
			
		||||
          This works for now, but is incredibly brittle. It's part of the reason we can't use testFixtures inside our
 | 
			
		||||
          MC projects, as that adds a project(self) -> test dependency, which would pull in the jar instead.
 | 
			
		||||
 | 
			
		||||
          Note we register a fake client jar here. It's not actually needed, but is there to make sure IDEA has
 | 
			
		||||
          a way to tell that client classes are needed at runtime.
 | 
			
		||||
 | 
			
		||||
          I'm so sorry, deeply aware how cursed this is.
 | 
			
		||||
        */
 | 
			
		||||
        setupOutgoing(main, "CommonOnly")
 | 
			
		||||
        project.tasks.register(client.jarTaskName, Jar::class.java) {
 | 
			
		||||
            description = "An empty jar standing in for the client classes."
 | 
			
		||||
            group = BasePlugin.BUILD_GROUP
 | 
			
		||||
            archiveClassifier.set("client")
 | 
			
		||||
        }
 | 
			
		||||
        setupOutgoing(client)
 | 
			
		||||
 | 
			
		||||
        MinecraftSetup(project).setupOutgoingConfigurations()
 | 
			
		||||
 | 
			
		||||
        // Reset the client classpath (Loom configures it slightly differently to this) and add a main -> client
 | 
			
		||||
        // dependency. Here we /can/ use source set outputs as we add transitive deps by patching the classpath. Nasty,
 | 
			
		||||
@@ -106,6 +82,12 @@ class MinecraftConfigurations private constructor(private val project: Project)
 | 
			
		||||
        project.tasks.named("jar", Jar::class.java) { from(client.output) }
 | 
			
		||||
        project.tasks.named("sourcesJar", Jar::class.java) { from(client.allSource) }
 | 
			
		||||
 | 
			
		||||
        setupBasic()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setupBasic() {
 | 
			
		||||
        val client = sourceSets["client"]
 | 
			
		||||
 | 
			
		||||
        project.extensions.configure(CCTweakedExtension::class.java) {
 | 
			
		||||
            sourceDirectories.add(SourceSetReference.internal(client))
 | 
			
		||||
        }
 | 
			
		||||
@@ -120,83 +102,19 @@ class MinecraftConfigurations private constructor(private val project: Project)
 | 
			
		||||
        project.tasks.named("check") { dependsOn(checkDependencyConsistency) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setupOutgoing(sourceSet: SourceSet, suffix: String = "") {
 | 
			
		||||
        setupOutgoing("${sourceSet.apiElementsConfigurationName}$suffix", sourceSet, objects.named(Usage.JAVA_API)) {
 | 
			
		||||
            description = "API elements for ${sourceSet.name}"
 | 
			
		||||
            extendsFrom(configurations[sourceSet.apiConfigurationName])
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setupOutgoing("${sourceSet.runtimeElementsConfigurationName}$suffix", sourceSet, objects.named(Usage.JAVA_RUNTIME)) {
 | 
			
		||||
            description = "Runtime elements for ${sourceSet.name}"
 | 
			
		||||
            extendsFrom(configurations[sourceSet.implementationConfigurationName], configurations[sourceSet.runtimeOnlyConfigurationName])
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set up an outgoing configuration for a specific source set. We set an additional "main" or "client" capability
 | 
			
		||||
     * (depending on the source set name) which allows downstream projects to consume them separately (see
 | 
			
		||||
     * [DependencyHandler.commonClasses] and [DependencyHandler.clientClasses]).
 | 
			
		||||
     */
 | 
			
		||||
    private fun setupOutgoing(name: String, sourceSet: SourceSet, usage: Usage, configure: Configuration.() -> Unit) {
 | 
			
		||||
        configurations.register(name) {
 | 
			
		||||
            isVisible = false
 | 
			
		||||
            isCanBeConsumed = true
 | 
			
		||||
            isCanBeResolved = false
 | 
			
		||||
 | 
			
		||||
            configure(this)
 | 
			
		||||
 | 
			
		||||
            attributes {
 | 
			
		||||
                attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
 | 
			
		||||
                attribute(Usage.USAGE_ATTRIBUTE, usage)
 | 
			
		||||
                attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
 | 
			
		||||
                attributeProvider(
 | 
			
		||||
                    TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE,
 | 
			
		||||
                    java.toolchain.languageVersion.map { it.asInt() },
 | 
			
		||||
                )
 | 
			
		||||
                attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.JAR))
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            outgoing {
 | 
			
		||||
                capability(BasicOutgoingCapability(project, sourceSet.name))
 | 
			
		||||
 | 
			
		||||
                // We have two outgoing variants here: the original jar and the classes.
 | 
			
		||||
                artifact(project.tasks.named(sourceSet.jarTaskName))
 | 
			
		||||
 | 
			
		||||
                variants.create("classes") {
 | 
			
		||||
                    attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.CLASSES))
 | 
			
		||||
                    sourceSet.output.classesDirs.forEach { artifact(it) { builtBy(sourceSet.output) } }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun setupBasic(project: Project) {
 | 
			
		||||
            MinecraftConfigurations(project).setupBasic()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        fun setup(project: Project) {
 | 
			
		||||
            MinecraftConfigurations(project).setup()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private class BasicIncomingCapability(private val module: ModuleDependency, private val name: String) : Capability {
 | 
			
		||||
    override fun getGroup(): String = module.group!!
 | 
			
		||||
    override fun getName(): String = "${module.name}-$name"
 | 
			
		||||
    override fun getVersion(): String? = null
 | 
			
		||||
}
 | 
			
		||||
fun DependencyHandler.clientClasses(notation: Any): ModuleDependency =
 | 
			
		||||
    Capabilities.clientClasses(create(notation) as ModuleDependency)
 | 
			
		||||
 | 
			
		||||
private class BasicOutgoingCapability(private val project: Project, private val name: String) : Capability {
 | 
			
		||||
    override fun getGroup(): String = project.group.toString()
 | 
			
		||||
    override fun getName(): String = "${project.name}-$name"
 | 
			
		||||
    override fun getVersion(): String = project.version.toString()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun DependencyHandler.clientClasses(notation: Any): ModuleDependency {
 | 
			
		||||
    val dep = create(notation) as ModuleDependency
 | 
			
		||||
    dep.capabilities { requireCapability(BasicIncomingCapability(dep, "client")) }
 | 
			
		||||
    return dep
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun DependencyHandler.commonClasses(notation: Any): ModuleDependency {
 | 
			
		||||
    val dep = create(notation) as ModuleDependency
 | 
			
		||||
    dep.capabilities { requireCapability(BasicIncomingCapability(dep, "main")) }
 | 
			
		||||
    return dep
 | 
			
		||||
}
 | 
			
		||||
fun DependencyHandler.commonClasses(notation: Any): ModuleDependency =
 | 
			
		||||
    Capabilities.commonClasses(create(notation) as ModuleDependency)
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ of the mod should run fine on later versions.
 | 
			
		||||
However, some changes to the underlying game, or CC: Tweaked's own internals may break some programs. This page serves
 | 
			
		||||
as documentation for breaking changes and "gotchas" one should look out for between versions.
 | 
			
		||||
 | 
			
		||||
## CC: Tweaked 1.109.0 to 1.109.2 {#cct-1.109}
 | 
			
		||||
## CC: Tweaked 1.109.0 to 1.109.3 {#cct-1.109}
 | 
			
		||||
 | 
			
		||||
 - Update to Lua 5.2:
 | 
			
		||||
   - Support for Lua 5.0's pseudo-argument `arg` has been removed. You should always use `...` for varargs.
 | 
			
		||||
@@ -76,6 +76,6 @@ as documentation for breaking changes and "gotchas" one should look out for betw
 | 
			
		||||
 - Programs containing `/` are looked up in the current directory and are no longer looked up on the path. For instance,
 | 
			
		||||
   you can no longer type `turtle/excavate` to run `/rom/programs/turtle/excavate.lua`.
 | 
			
		||||
 | 
			
		||||
[flattening]: https://minecraft.wiki.com/w/Java_Edition_1.13/Flattening
 | 
			
		||||
[flattening]: https://minecraft.wiki/w/Java_Edition_1.13/Flattening
 | 
			
		||||
[legal_data_pack]: https://minecraft.gamepedia.com/Tutorials/Creating_a_data_pack#Legal_characters
 | 
			
		||||
[datapack-example]: https://github.com/cc-tweaked/datapack-example "An example datapack for CC: Tweaked"
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ kotlin.jvm.target.validation.mode=error
 | 
			
		||||
 | 
			
		||||
# Mod properties
 | 
			
		||||
isUnstable=false
 | 
			
		||||
modVersion=1.109.3
 | 
			
		||||
modVersion=1.109.6
 | 
			
		||||
 | 
			
		||||
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
 | 
			
		||||
mcVersion=1.20.1
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ forgeSpi = "7.0.1"
 | 
			
		||||
mixin = "0.8.5"
 | 
			
		||||
parchment = "2023.08.20"
 | 
			
		||||
parchmentMc = "1.20.1"
 | 
			
		||||
yarn = "1.20.1+build.10"
 | 
			
		||||
 | 
			
		||||
# Core dependencies (these versions are tied to the version Minecraft uses)
 | 
			
		||||
fastutil = "8.5.9"
 | 
			
		||||
@@ -25,7 +26,7 @@ slf4j = "2.0.1"
 | 
			
		||||
asm = "9.6"
 | 
			
		||||
autoService = "1.1.1"
 | 
			
		||||
checkerFramework = "3.42.0"
 | 
			
		||||
cobalt = "0.8.2"
 | 
			
		||||
cobalt = "0.9.1"
 | 
			
		||||
commonsCli = "1.6.0"
 | 
			
		||||
jetbrainsAnnotations = "24.1.0"
 | 
			
		||||
jsr305 = "3.0.2"
 | 
			
		||||
@@ -57,8 +58,8 @@ checkstyle = "10.12.6"
 | 
			
		||||
curseForgeGradle = "1.0.14"
 | 
			
		||||
errorProne-core = "2.23.0"
 | 
			
		||||
errorProne-plugin = "3.1.0"
 | 
			
		||||
fabric-loom = "1.3.9"
 | 
			
		||||
forgeGradle = "6.0.8"
 | 
			
		||||
fabric-loom = "1.5.7"
 | 
			
		||||
forgeGradle = "6.0.20"
 | 
			
		||||
githubRelease = "2.5.2"
 | 
			
		||||
gradleVersions = "0.50.0"
 | 
			
		||||
ideaExt = "1.1.7"
 | 
			
		||||
@@ -71,9 +72,8 @@ nullAway = "0.9.9"
 | 
			
		||||
spotless = "6.23.3"
 | 
			
		||||
taskTree = "2.1.1"
 | 
			
		||||
teavm = "0.10.0-SQUID.2"
 | 
			
		||||
vanillaGradle = "0.2.1-SNAPSHOT"
 | 
			
		||||
vanillaExtract = "0.1.1"
 | 
			
		||||
versionCatalogUpdate = "0.8.1"
 | 
			
		||||
vineflower = "1.11.0"
 | 
			
		||||
 | 
			
		||||
[libraries]
 | 
			
		||||
# Normal dependencies
 | 
			
		||||
@@ -159,8 +159,8 @@ teavm-metaprogramming-api = { module = "org.teavm:teavm-metaprogramming-api", ve
 | 
			
		||||
teavm-metaprogramming-impl = { module = "org.teavm:teavm-metaprogramming-impl", version.ref = "teavm" }
 | 
			
		||||
teavm-platform = { module = "org.teavm:teavm-platform", version.ref = "teavm" }
 | 
			
		||||
teavm-tooling = { module = "org.teavm:teavm-tooling", version.ref = "teavm" }
 | 
			
		||||
vanillaGradle = { module = "org.spongepowered:vanillagradle", version.ref = "vanillaGradle" }
 | 
			
		||||
vineflower = { module = "io.github.juuxel:loom-vineflower", version.ref = "vineflower" }
 | 
			
		||||
vanillaExtract = { module = "cc.tweaked.vanilla-extract:plugin", version.ref = "vanillaExtract" }
 | 
			
		||||
yarn = { module = "net.fabricmc:yarn", version.ref = "yarn" }
 | 
			
		||||
 | 
			
		||||
[plugins]
 | 
			
		||||
forgeGradle = { id = "net.minecraftforge.gradle", version.ref = "forgeGradle" }
 | 
			
		||||
@@ -180,7 +180,6 @@ kotlin = ["kotlin-stdlib", "kotlin-coroutines"]
 | 
			
		||||
externalMods-common = ["jei-api", "nightConfig-core", "nightConfig-toml"]
 | 
			
		||||
externalMods-forge-compile = ["moreRed", "oculus", "jei-api"]
 | 
			
		||||
externalMods-forge-runtime = ["jei-forge"]
 | 
			
		||||
externalMods-fabric = ["nightConfig-core", "nightConfig-toml"]
 | 
			
		||||
externalMods-fabric-compile = ["fabricPermissions", "iris", "jei-api", "rei-api", "rei-builtin"]
 | 
			
		||||
externalMods-fabric-runtime = ["jei-fabric", "modmenu"]
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								gradle/wrapper/gradle-wrapper.jar
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								gradle/wrapper/gradle-wrapper.jar
									
									
									
									
										vendored
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										3
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							@@ -1,6 +1,7 @@
 | 
			
		||||
distributionBase=GRADLE_USER_HOME
 | 
			
		||||
distributionPath=wrapper/dists
 | 
			
		||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
 | 
			
		||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
 | 
			
		||||
networkTimeout=10000
 | 
			
		||||
validateDistributionUrl=true
 | 
			
		||||
zipStoreBase=GRADLE_USER_HOME
 | 
			
		||||
zipStorePath=wrapper/dists
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								gradlew
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								gradlew
									
									
									
									
										vendored
									
									
								
							@@ -83,7 +83,8 @@ done
 | 
			
		||||
# This is normally unused
 | 
			
		||||
# shellcheck disable=SC2034
 | 
			
		||||
APP_BASE_NAME=${0##*/}
 | 
			
		||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
 | 
			
		||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
 | 
			
		||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
 | 
			
		||||
 | 
			
		||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
 | 
			
		||||
MAX_FD=maximum
 | 
			
		||||
@@ -130,10 +131,13 @@ location of your Java installation."
 | 
			
		||||
    fi
 | 
			
		||||
else
 | 
			
		||||
    JAVACMD=java
 | 
			
		||||
    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
 | 
			
		||||
    if ! command -v java >/dev/null 2>&1
 | 
			
		||||
    then
 | 
			
		||||
        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
 | 
			
		||||
 | 
			
		||||
Please set the JAVA_HOME variable in your environment to match the
 | 
			
		||||
location of your Java installation."
 | 
			
		||||
    fi
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# Increase the maximum file descriptors if we can.
 | 
			
		||||
@@ -141,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
 | 
			
		||||
    case $MAX_FD in #(
 | 
			
		||||
      max*)
 | 
			
		||||
        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
 | 
			
		||||
        # shellcheck disable=SC3045
 | 
			
		||||
        # shellcheck disable=SC2039,SC3045
 | 
			
		||||
        MAX_FD=$( ulimit -H -n ) ||
 | 
			
		||||
            warn "Could not query maximum file descriptor limit"
 | 
			
		||||
    esac
 | 
			
		||||
@@ -149,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
 | 
			
		||||
      '' | soft) :;; #(
 | 
			
		||||
      *)
 | 
			
		||||
        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
 | 
			
		||||
        # shellcheck disable=SC3045
 | 
			
		||||
        # shellcheck disable=SC2039,SC3045
 | 
			
		||||
        ulimit -n "$MAX_FD" ||
 | 
			
		||||
            warn "Could not set maximum file descriptor limit to $MAX_FD"
 | 
			
		||||
    esac
 | 
			
		||||
@@ -198,11 +202,11 @@ fi
 | 
			
		||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
 | 
			
		||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
 | 
			
		||||
 | 
			
		||||
# Collect all arguments for the java command;
 | 
			
		||||
#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
 | 
			
		||||
#     shell script including quotes and variable substitutions, so put them in
 | 
			
		||||
#     double quotes to make sure that they get re-expanded; and
 | 
			
		||||
#   * put everything else in single quotes, so that it's not re-expanded.
 | 
			
		||||
# Collect all arguments for the java command:
 | 
			
		||||
#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
 | 
			
		||||
#     and any embedded shellness will be escaped.
 | 
			
		||||
#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
 | 
			
		||||
#     treated as '${Hostname}' itself on the command line.
 | 
			
		||||
 | 
			
		||||
set -- \
 | 
			
		||||
        "-Dorg.gradle.appname=$APP_BASE_NAME" \
 | 
			
		||||
 
 | 
			
		||||
@@ -105,6 +105,10 @@
 | 
			
		||||
   /projects/core/src/main/resources/data/computercraft/lua/rom/apis/turtle/turtle.lua)
 | 
			
		||||
  (linters -var:deprecated))
 | 
			
		||||
 | 
			
		||||
;; Suppress unused variable warnings in the parser.
 | 
			
		||||
(at /projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/parser.lua
 | 
			
		||||
  (linters -var:unused))
 | 
			
		||||
 | 
			
		||||
(at /projects/core/src/test/resources/test-rom
 | 
			
		||||
  ; We should still be able to test deprecated members.
 | 
			
		||||
  (linters -var:deprecated)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1376
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1376
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -23,7 +23,7 @@
 | 
			
		||||
    "rehype-highlight": "^7.0.0",
 | 
			
		||||
    "rehype-react": "^8.0.0",
 | 
			
		||||
    "rollup": "^4.0.0",
 | 
			
		||||
    "tsx": "^3.12.10",
 | 
			
		||||
    "tsx": "^4.7.0",
 | 
			
		||||
    "typescript": "^5.2.2"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -27,8 +27,13 @@ public final class ComputerCraftAPIClient {
 | 
			
		||||
     * @param serialiser The turtle upgrade serialiser.
 | 
			
		||||
     * @param modeller   The upgrade modeller.
 | 
			
		||||
     * @param <T>        The type of the turtle upgrade.
 | 
			
		||||
     * @deprecated This method can lead to confusing load behaviour on Forge. Use
 | 
			
		||||
     * {@code dan200.computercraft.api.client.FabricComputerCraftAPIClient#registerTurtleUpgradeModeller} on Fabric, or
 | 
			
		||||
     * {@code dan200.computercraft.api.client.turtle.RegisterTurtleModellersEvent} on Forge.
 | 
			
		||||
     */
 | 
			
		||||
    @Deprecated(forRemoval = true)
 | 
			
		||||
    public static <T extends ITurtleUpgrade> void registerTurtleUpgradeModeller(TurtleUpgradeSerialiser<T> serialiser, TurtleUpgradeModeller<T> modeller) {
 | 
			
		||||
        // TODO(1.20.4): Remove this
 | 
			
		||||
        getInstance().registerTurtleUpgradeModeller(serialiser, modeller);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,26 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.client.turtle;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
 | 
			
		||||
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A functional interface to register a {@link TurtleUpgradeModeller} for a class of turtle upgrades.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This interface is largely intended to be used from multi-loader code, to allow sharing registration code between
 | 
			
		||||
 * multiple loaders.
 | 
			
		||||
 */
 | 
			
		||||
@FunctionalInterface
 | 
			
		||||
public interface RegisterTurtleUpgradeModeller {
 | 
			
		||||
    /**
 | 
			
		||||
     * Register a {@link TurtleUpgradeModeller}.
 | 
			
		||||
     *
 | 
			
		||||
     * @param serialiser The turtle upgrade serialiser.
 | 
			
		||||
     * @param modeller   The upgrade modeller.
 | 
			
		||||
     * @param <T>        The type of the turtle upgrade.
 | 
			
		||||
     */
 | 
			
		||||
    <T extends ITurtleUpgrade> void register(TurtleUpgradeSerialiser<T> serialiser, TurtleUpgradeModeller<T> modeller);
 | 
			
		||||
}
 | 
			
		||||
@@ -4,12 +4,10 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.client.turtle;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.client.ComputerCraftAPIClient;
 | 
			
		||||
import dan200.computercraft.api.client.TransformedModel;
 | 
			
		||||
import dan200.computercraft.api.turtle.ITurtleAccess;
 | 
			
		||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
 | 
			
		||||
import dan200.computercraft.api.turtle.TurtleSide;
 | 
			
		||||
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
 | 
			
		||||
import net.minecraft.client.resources.model.ModelResourceLocation;
 | 
			
		||||
import net.minecraft.client.resources.model.UnbakedModel;
 | 
			
		||||
import net.minecraft.nbt.CompoundTag;
 | 
			
		||||
@@ -21,9 +19,13 @@ import java.util.List;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provides models for a {@link ITurtleUpgrade}.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Use {@code dan200.computercraft.api.client.FabricComputerCraftAPIClient#registerTurtleUpgradeModeller} to register a
 | 
			
		||||
 * modeller on Fabric and {@code dan200.computercraft.api.client.turtle.RegisterTurtleModellersEvent} to register one
 | 
			
		||||
 * on Forge
 | 
			
		||||
 *
 | 
			
		||||
 * @param <T> The type of turtle upgrade this modeller applies to.
 | 
			
		||||
 * @see ComputerCraftAPIClient#registerTurtleUpgradeModeller(TurtleUpgradeSerialiser, TurtleUpgradeModeller) To register a modeller.
 | 
			
		||||
 * @see RegisterTurtleUpgradeModeller For multi-loader registration support.
 | 
			
		||||
 */
 | 
			
		||||
public interface TurtleUpgradeModeller<T extends ITurtleUpgrade> {
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -48,13 +48,8 @@ import java.util.function.Function;
 | 
			
		||||
 * }
 | 
			
		||||
 * }</pre>
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Finally, we need to register a model for our upgrade. This is done with
 | 
			
		||||
 * {@link dan200.computercraft.api.client.ComputerCraftAPIClient#registerTurtleUpgradeModeller}:
 | 
			
		||||
 *
 | 
			
		||||
 * <pre>{@code
 | 
			
		||||
 * // Register our model inside FMLClientSetupEvent
 | 
			
		||||
 * ComputerCraftAPIClient.registerTurtleUpgradeModeller(MY_UPGRADE.get(), TurtleUpgradeModeller.flatItem())
 | 
			
		||||
 * }</pre>
 | 
			
		||||
 * Finally, we need to register a model for our upgrade. The way to do this varies on mod loader, see
 | 
			
		||||
 * {@link dan200.computercraft.api.client.turtle.TurtleUpgradeModeller} for more information.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * {@link TurtleUpgradeDataProvider} provides a data provider to aid with generating these JSON files.
 | 
			
		||||
 *
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,14 @@ configurations {
 | 
			
		||||
    register("cctJavadoc")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
repositories {
 | 
			
		||||
    maven("https://maven.minecraftforge.net/") {
 | 
			
		||||
        content {
 | 
			
		||||
            includeModule("org.spongepowered", "mixin")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
dependencies {
 | 
			
		||||
    // Pull in our other projects. See comments in MinecraftConfigurations on this nastiness.
 | 
			
		||||
    implementation(project(":core"))
 | 
			
		||||
@@ -31,7 +39,6 @@ dependencies {
 | 
			
		||||
    compileOnly(libs.bundles.externalMods.common)
 | 
			
		||||
    clientCompileOnly(variantOf(libs.emi) { classifier("api") })
 | 
			
		||||
 | 
			
		||||
    compileOnly(libs.mixin)
 | 
			
		||||
    annotationProcessorEverywhere(libs.autoService)
 | 
			
		||||
    testFixturesAnnotationProcessor(libs.autoService)
 | 
			
		||||
 | 
			
		||||
@@ -39,6 +46,7 @@ dependencies {
 | 
			
		||||
    testImplementation(libs.bundles.test)
 | 
			
		||||
    testRuntimeOnly(libs.bundles.testRuntime)
 | 
			
		||||
 | 
			
		||||
    testModCompileOnly(libs.mixin)
 | 
			
		||||
    testModImplementation(testFixtures(project(":core")))
 | 
			
		||||
    testModImplementation(testFixtures(project(":common")))
 | 
			
		||||
    testModImplementation(libs.bundles.kotlin)
 | 
			
		||||
@@ -72,7 +80,7 @@ val luaJavadoc by tasks.registering(Javadoc::class) {
 | 
			
		||||
 | 
			
		||||
    javadocTool.set(
 | 
			
		||||
        javaToolchains.javadocToolFor {
 | 
			
		||||
            languageVersion.set(cc.tweaked.gradle.CCTweakedPlugin.JAVA_VERSION)
 | 
			
		||||
            languageVersion.set(CCTweakedPlugin.JAVA_VERSION)
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -17,8 +17,6 @@ import dan200.computercraft.client.render.monitor.MonitorRenderState;
 | 
			
		||||
import dan200.computercraft.client.sound.SpeakerManager;
 | 
			
		||||
import dan200.computercraft.shared.CommonHooks;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import dan200.computercraft.shared.command.CommandComputerCraft;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ServerContext;
 | 
			
		||||
import dan200.computercraft.shared.media.items.PrintoutItem;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.modem.wired.CableBlock;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.modem.wired.CableModemVariant;
 | 
			
		||||
@@ -28,7 +26,6 @@ import dan200.computercraft.shared.pocket.items.PocketComputerItem;
 | 
			
		||||
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
 | 
			
		||||
import dan200.computercraft.shared.util.PauseAwareTimer;
 | 
			
		||||
import dan200.computercraft.shared.util.WorldUtil;
 | 
			
		||||
import net.minecraft.Util;
 | 
			
		||||
import net.minecraft.client.Camera;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.client.renderer.MultiBufferSource;
 | 
			
		||||
@@ -43,7 +40,6 @@ import net.minecraft.world.phys.BlockHitResult;
 | 
			
		||||
import net.minecraft.world.phys.HitResult;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -71,10 +67,6 @@ public final class ClientHooks {
 | 
			
		||||
        ClientPocketComputers.reset();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static boolean onChatMessage(String message) {
 | 
			
		||||
        return handleOpenComputerCommand(message);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static boolean drawHighlight(PoseStack transform, MultiBufferSource bufferSource, Camera camera, BlockHitResult hit) {
 | 
			
		||||
        return CableHighlightRenderer.drawHighlight(transform, bufferSource, camera, hit)
 | 
			
		||||
            || MonitorHighlightRenderer.drawHighlight(transform, bufferSource, camera, hit);
 | 
			
		||||
@@ -109,34 +101,6 @@ public final class ClientHooks {
 | 
			
		||||
        SpeakerManager.onPlayStreaming(engine, channel, stream);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handle the {@link CommandComputerCraft#OPEN_COMPUTER} "clientside command". This isn't a true command, as we
 | 
			
		||||
     * don't want it to actually be visible to the user.
 | 
			
		||||
     *
 | 
			
		||||
     * @param message The current chat message.
 | 
			
		||||
     * @return Whether to cancel sending this message.
 | 
			
		||||
     */
 | 
			
		||||
    private static boolean handleOpenComputerCommand(String message) {
 | 
			
		||||
        if (!message.startsWith(CommandComputerCraft.OPEN_COMPUTER)) return false;
 | 
			
		||||
 | 
			
		||||
        var server = Minecraft.getInstance().getSingleplayerServer();
 | 
			
		||||
        if (server == null) return false;
 | 
			
		||||
 | 
			
		||||
        var idStr = message.substring(CommandComputerCraft.OPEN_COMPUTER.length()).trim();
 | 
			
		||||
        int id;
 | 
			
		||||
        try {
 | 
			
		||||
            id = Integer.parseInt(idStr);
 | 
			
		||||
        } catch (NumberFormatException ignore) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var file = new File(ServerContext.get(server).storageDir().toFile(), "computer/" + id);
 | 
			
		||||
        if (!file.isDirectory()) return false;
 | 
			
		||||
 | 
			
		||||
        Util.getPlatform().openFile(file);
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add additional information about the currently targeted block to the debug screen.
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,12 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client;
 | 
			
		||||
 | 
			
		||||
import com.mojang.brigadier.CommandDispatcher;
 | 
			
		||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
 | 
			
		||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
 | 
			
		||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.api.client.ComputerCraftAPIClient;
 | 
			
		||||
import dan200.computercraft.api.client.turtle.RegisterTurtleUpgradeModeller;
 | 
			
		||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
 | 
			
		||||
import dan200.computercraft.client.gui.*;
 | 
			
		||||
import dan200.computercraft.client.pocket.ClientPocketComputers;
 | 
			
		||||
@@ -16,11 +20,14 @@ import dan200.computercraft.client.turtle.TurtleModemModeller;
 | 
			
		||||
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
 | 
			
		||||
import dan200.computercraft.core.util.Colour;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import dan200.computercraft.shared.command.CommandComputerCraft;
 | 
			
		||||
import dan200.computercraft.shared.common.IColouredItem;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ServerContext;
 | 
			
		||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
 | 
			
		||||
import dan200.computercraft.shared.computer.inventory.ViewComputerMenu;
 | 
			
		||||
import dan200.computercraft.shared.media.items.DiskItem;
 | 
			
		||||
import dan200.computercraft.shared.media.items.TreasureDiskItem;
 | 
			
		||||
import net.minecraft.Util;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.client.color.item.ItemColor;
 | 
			
		||||
import net.minecraft.client.gui.screens.MenuScreens;
 | 
			
		||||
@@ -30,6 +37,7 @@ import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
 | 
			
		||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderers;
 | 
			
		||||
import net.minecraft.client.renderer.item.ClampedItemPropertyFunction;
 | 
			
		||||
import net.minecraft.client.renderer.item.ItemProperties;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.server.packs.resources.PreparableReloadListener;
 | 
			
		||||
import net.minecraft.server.packs.resources.ResourceProvider;
 | 
			
		||||
@@ -39,6 +47,7 @@ import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.level.ItemLike;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.util.function.BiConsumer;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
@@ -60,18 +69,6 @@ public final class ClientRegistry {
 | 
			
		||||
     * Register any client-side objects which don't have to be done on the main thread.
 | 
			
		||||
     */
 | 
			
		||||
    public static void register() {
 | 
			
		||||
        ComputerCraftAPIClient.registerTurtleUpgradeModeller(ModRegistry.TurtleSerialisers.SPEAKER.get(), TurtleUpgradeModeller.sided(
 | 
			
		||||
            new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_left"),
 | 
			
		||||
            new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_right")
 | 
			
		||||
        ));
 | 
			
		||||
        ComputerCraftAPIClient.registerTurtleUpgradeModeller(ModRegistry.TurtleSerialisers.WORKBENCH.get(), TurtleUpgradeModeller.sided(
 | 
			
		||||
            new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_left"),
 | 
			
		||||
            new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_right")
 | 
			
		||||
        ));
 | 
			
		||||
        ComputerCraftAPIClient.registerTurtleUpgradeModeller(ModRegistry.TurtleSerialisers.WIRELESS_MODEM_NORMAL.get(), new TurtleModemModeller(false));
 | 
			
		||||
        ComputerCraftAPIClient.registerTurtleUpgradeModeller(ModRegistry.TurtleSerialisers.WIRELESS_MODEM_ADVANCED.get(), new TurtleModemModeller(true));
 | 
			
		||||
        ComputerCraftAPIClient.registerTurtleUpgradeModeller(ModRegistry.TurtleSerialisers.TOOL.get(), TurtleUpgradeModeller.flatItem());
 | 
			
		||||
 | 
			
		||||
        BlockEntityRenderers.register(ModRegistry.BlockEntities.MONITOR_NORMAL.get(), MonitorBlockEntityRenderer::new);
 | 
			
		||||
        BlockEntityRenderers.register(ModRegistry.BlockEntities.MONITOR_ADVANCED.get(), MonitorBlockEntityRenderer::new);
 | 
			
		||||
        BlockEntityRenderers.register(ModRegistry.BlockEntities.TURTLE_NORMAL.get(), TurtleBlockEntityRenderer::new);
 | 
			
		||||
@@ -103,6 +100,20 @@ public final class ClientRegistry {
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void registerTurtleModellers(RegisterTurtleUpgradeModeller register) {
 | 
			
		||||
        register.register(ModRegistry.TurtleSerialisers.SPEAKER.get(), TurtleUpgradeModeller.sided(
 | 
			
		||||
            new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_left"),
 | 
			
		||||
            new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_right")
 | 
			
		||||
        ));
 | 
			
		||||
        register.register(ModRegistry.TurtleSerialisers.WORKBENCH.get(), TurtleUpgradeModeller.sided(
 | 
			
		||||
            new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_left"),
 | 
			
		||||
            new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_right")
 | 
			
		||||
        ));
 | 
			
		||||
        register.register(ModRegistry.TurtleSerialisers.WIRELESS_MODEM_NORMAL.get(), new TurtleModemModeller(false));
 | 
			
		||||
        register.register(ModRegistry.TurtleSerialisers.WIRELESS_MODEM_ADVANCED.get(), new TurtleModemModeller(true));
 | 
			
		||||
        register.register(ModRegistry.TurtleSerialisers.TOOL.get(), TurtleUpgradeModeller.flatItem());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @SafeVarargs
 | 
			
		||||
    private static void registerItemProperty(String name, ClampedItemPropertyFunction getter, Supplier<? extends Item>... items) {
 | 
			
		||||
        var id = new ResourceLocation(ComputerCraftAPI.MOD_ID, name);
 | 
			
		||||
@@ -179,4 +190,45 @@ public final class ClientRegistry {
 | 
			
		||||
            return function.unclampedCall(stack, level, entity, layer);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Register client-side commands.
 | 
			
		||||
     *
 | 
			
		||||
     * @param dispatcher The dispatcher to register the commands to.
 | 
			
		||||
     * @param sendError  A function to send an error message.
 | 
			
		||||
     * @param <T>        The type of the client-side command context.
 | 
			
		||||
     */
 | 
			
		||||
    public static <T> void registerClientCommands(CommandDispatcher<T> dispatcher, BiConsumer<T, Component> sendError) {
 | 
			
		||||
        dispatcher.register(LiteralArgumentBuilder.<T>literal(CommandComputerCraft.CLIENT_OPEN_FOLDER)
 | 
			
		||||
            .requires(x -> Minecraft.getInstance().getSingleplayerServer() != null)
 | 
			
		||||
            .then(RequiredArgumentBuilder.<T, Integer>argument("computer_id", IntegerArgumentType.integer(0))
 | 
			
		||||
                .executes(c -> handleOpenComputerCommand(c.getSource(), sendError, c.getArgument("computer_id", Integer.class)))
 | 
			
		||||
            ));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handle the {@link CommandComputerCraft#CLIENT_OPEN_FOLDER} command.
 | 
			
		||||
     *
 | 
			
		||||
     * @param context   The command context.
 | 
			
		||||
     * @param sendError A function to send an error message.
 | 
			
		||||
     * @param id        The computer's id.
 | 
			
		||||
     * @param <T>       The type of the client-side command context.
 | 
			
		||||
     * @return {@code 1} if a folder was opened, {@code 0} otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    private static <T> int handleOpenComputerCommand(T context, BiConsumer<T, Component> sendError, int id) {
 | 
			
		||||
        var server = Minecraft.getInstance().getSingleplayerServer();
 | 
			
		||||
        if (server == null) {
 | 
			
		||||
            sendError.accept(context, Component.literal("Not on a single-player server"));
 | 
			
		||||
            return 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var file = new File(ServerContext.get(server).storageDir().toFile(), "computer/" + id);
 | 
			
		||||
        if (!file.isDirectory()) {
 | 
			
		||||
            sendError.accept(context, Component.literal("Computer's folder does not exist"));
 | 
			
		||||
            return 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Util.getPlatform().openFile(file);
 | 
			
		||||
        return 1;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ package dan200.computercraft.client.gui;
 | 
			
		||||
import dan200.computercraft.client.gui.widgets.ComputerSidebar;
 | 
			
		||||
import dan200.computercraft.client.gui.widgets.DynamicImageButton;
 | 
			
		||||
import dan200.computercraft.client.gui.widgets.TerminalWidget;
 | 
			
		||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
 | 
			
		||||
import dan200.computercraft.client.network.ClientNetworking;
 | 
			
		||||
import dan200.computercraft.core.terminal.Terminal;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.InputHandler;
 | 
			
		||||
@@ -207,7 +207,7 @@ public abstract class AbstractComputerScreen<T extends AbstractComputerMenu> ext
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (toUpload.size() > 0) UploadFileMessage.send(menu, toUpload, ClientPlatformHelper.get()::sendToServer);
 | 
			
		||||
        if (toUpload.size() > 0) UploadFileMessage.send(menu, toUpload, ClientNetworking::sendToServer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void uploadResult(UploadResult result, @Nullable Component message) {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.gui;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
 | 
			
		||||
import dan200.computercraft.client.network.ClientNetworking;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.InputHandler;
 | 
			
		||||
import dan200.computercraft.shared.computer.menu.ComputerMenu;
 | 
			
		||||
import dan200.computercraft.shared.network.server.ComputerActionServerMessage;
 | 
			
		||||
@@ -29,51 +29,51 @@ public final class ClientInputHandler implements InputHandler {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void turnOn() {
 | 
			
		||||
        ClientPlatformHelper.get().sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TURN_ON));
 | 
			
		||||
        ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TURN_ON));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void shutdown() {
 | 
			
		||||
        ClientPlatformHelper.get().sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.SHUTDOWN));
 | 
			
		||||
        ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.SHUTDOWN));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void reboot() {
 | 
			
		||||
        ClientPlatformHelper.get().sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.REBOOT));
 | 
			
		||||
        ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.REBOOT));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void queueEvent(String event, @Nullable Object[] arguments) {
 | 
			
		||||
        ClientPlatformHelper.get().sendToServer(new QueueEventServerMessage(menu, event, arguments));
 | 
			
		||||
        ClientNetworking.sendToServer(new QueueEventServerMessage(menu, event, arguments));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void keyDown(int key, boolean repeat) {
 | 
			
		||||
        ClientPlatformHelper.get().sendToServer(new KeyEventServerMessage(menu, repeat ? KeyEventServerMessage.TYPE_REPEAT : KeyEventServerMessage.TYPE_DOWN, key));
 | 
			
		||||
        ClientNetworking.sendToServer(new KeyEventServerMessage(menu, repeat ? KeyEventServerMessage.TYPE_REPEAT : KeyEventServerMessage.TYPE_DOWN, key));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void keyUp(int key) {
 | 
			
		||||
        ClientPlatformHelper.get().sendToServer(new KeyEventServerMessage(menu, KeyEventServerMessage.TYPE_UP, key));
 | 
			
		||||
        ClientNetworking.sendToServer(new KeyEventServerMessage(menu, KeyEventServerMessage.TYPE_UP, key));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void mouseClick(int button, int x, int y) {
 | 
			
		||||
        ClientPlatformHelper.get().sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_CLICK, button, x, y));
 | 
			
		||||
        ClientNetworking.sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_CLICK, button, x, y));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void mouseUp(int button, int x, int y) {
 | 
			
		||||
        ClientPlatformHelper.get().sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_UP, button, x, y));
 | 
			
		||||
        ClientNetworking.sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_UP, button, x, y));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void mouseDrag(int button, int x, int y) {
 | 
			
		||||
        ClientPlatformHelper.get().sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_DRAG, button, x, y));
 | 
			
		||||
        ClientNetworking.sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_DRAG, button, x, y));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void mouseScroll(int direction, int x, int y) {
 | 
			
		||||
        ClientPlatformHelper.get().sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_SCROLL, direction, x, y));
 | 
			
		||||
        ClientNetworking.sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_SCROLL, direction, x, y));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,28 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.network;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.server.ServerNetworkContext;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Methods for sending packets from clients to the server.
 | 
			
		||||
 */
 | 
			
		||||
public final class ClientNetworking {
 | 
			
		||||
    private ClientNetworking() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send a network message to the server.
 | 
			
		||||
     *
 | 
			
		||||
     * @param message The message to send.
 | 
			
		||||
     */
 | 
			
		||||
    public static void sendToServer(NetworkMessage<ServerNetworkContext> message) {
 | 
			
		||||
        var connection = Minecraft.getInstance().getConnection();
 | 
			
		||||
        if (connection != null) connection.send(ClientPlatformHelper.get().createPacket(message));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.platform;
 | 
			
		||||
 | 
			
		||||
import com.google.auto.service.AutoService;
 | 
			
		||||
import dan200.computercraft.client.ClientTableFormatter;
 | 
			
		||||
import dan200.computercraft.client.gui.AbstractComputerScreen;
 | 
			
		||||
import dan200.computercraft.client.gui.OptionScreen;
 | 
			
		||||
@@ -17,30 +18,30 @@ import dan200.computercraft.shared.computer.upload.UploadResult;
 | 
			
		||||
import dan200.computercraft.shared.network.client.ClientNetworkContext;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
 | 
			
		||||
import io.netty.buffer.ByteBuf;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.sounds.SoundEvent;
 | 
			
		||||
import net.minecraft.world.entity.player.Player;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The base implementation of {@link ClientNetworkContext}.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This should be extended by mod loader specific modules with the remaining abstract methods.
 | 
			
		||||
 * The client-side implementation of {@link ClientNetworkContext}.
 | 
			
		||||
 */
 | 
			
		||||
public abstract class AbstractClientNetworkContext implements ClientNetworkContext {
 | 
			
		||||
@AutoService(ClientNetworkContext.class)
 | 
			
		||||
public final class ClientNetworkContextImpl implements ClientNetworkContext {
 | 
			
		||||
    @Override
 | 
			
		||||
    public final void handleChatTable(TableBuilder table) {
 | 
			
		||||
    public void handleChatTable(TableBuilder table) {
 | 
			
		||||
        ClientTableFormatter.INSTANCE.display(table);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final void handleComputerTerminal(int containerId, TerminalState terminal) {
 | 
			
		||||
    public void handleComputerTerminal(int containerId, TerminalState terminal) {
 | 
			
		||||
        Player player = Minecraft.getInstance().player;
 | 
			
		||||
        if (player != null && player.containerMenu.containerId == containerId && player.containerMenu instanceof ComputerMenu menu) {
 | 
			
		||||
            menu.updateTerminal(terminal);
 | 
			
		||||
@@ -48,7 +49,7 @@ public abstract class AbstractClientNetworkContext implements ClientNetworkConte
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final void handleMonitorData(BlockPos pos, TerminalState terminal) {
 | 
			
		||||
    public void handleMonitorData(BlockPos pos, TerminalState terminal) {
 | 
			
		||||
        var player = Minecraft.getInstance().player;
 | 
			
		||||
        if (player == null) return;
 | 
			
		||||
 | 
			
		||||
@@ -59,44 +60,46 @@ public abstract class AbstractClientNetworkContext implements ClientNetworkConte
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final void handlePocketComputerData(int instanceId, ComputerState state, int lightState, TerminalState terminal) {
 | 
			
		||||
    public void handlePlayRecord(BlockPos pos, @Nullable SoundEvent sound, @Nullable String name) {
 | 
			
		||||
        var mc = Minecraft.getInstance();
 | 
			
		||||
        ClientPlatformHelper.get().playStreamingMusic(pos, sound);
 | 
			
		||||
        if (name != null) mc.gui.setNowPlaying(Component.literal(name));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void handlePocketComputerData(int instanceId, ComputerState state, int lightState, TerminalState terminal) {
 | 
			
		||||
        var computer = ClientPocketComputers.get(instanceId, terminal.colour);
 | 
			
		||||
        computer.setState(state, lightState);
 | 
			
		||||
        if (terminal.hasTerminal()) computer.setTerminal(terminal);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final void handlePocketComputerDeleted(int instanceId) {
 | 
			
		||||
    public void handlePocketComputerDeleted(int instanceId) {
 | 
			
		||||
        ClientPocketComputers.remove(instanceId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume) {
 | 
			
		||||
        SpeakerManager.getSound(source).playAudio(reifyPosition(position), volume);
 | 
			
		||||
    public void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, ByteBuffer buffer) {
 | 
			
		||||
        SpeakerManager.getSound(source).playAudio(reifyPosition(position), volume, buffer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final void handleSpeakerAudioPush(UUID source, ByteBuf buffer) {
 | 
			
		||||
        SpeakerManager.getSound(source).pushAudio(buffer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final void handleSpeakerMove(UUID source, SpeakerPosition.Message position) {
 | 
			
		||||
    public void handleSpeakerMove(UUID source, SpeakerPosition.Message position) {
 | 
			
		||||
        SpeakerManager.moveSound(source, reifyPosition(position));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final void handleSpeakerPlay(UUID source, SpeakerPosition.Message position, ResourceLocation sound, float volume, float pitch) {
 | 
			
		||||
    public void handleSpeakerPlay(UUID source, SpeakerPosition.Message position, ResourceLocation sound, float volume, float pitch) {
 | 
			
		||||
        SpeakerManager.getSound(source).playSound(reifyPosition(position), sound, volume, pitch);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final void handleSpeakerStop(UUID source) {
 | 
			
		||||
    public void handleSpeakerStop(UUID source) {
 | 
			
		||||
        SpeakerManager.stopSound(source);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final void handleUploadResult(int containerId, UploadResult result, @Nullable Component errorMessage) {
 | 
			
		||||
    public void handleUploadResult(int containerId, UploadResult result, @Nullable Component errorMessage) {
 | 
			
		||||
        var minecraft = Minecraft.getInstance();
 | 
			
		||||
 | 
			
		||||
        var screen = OptionScreen.unwrap(minecraft.screen);
 | 
			
		||||
@@ -9,6 +9,10 @@ import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.server.ServerNetworkContext;
 | 
			
		||||
import net.minecraft.client.renderer.MultiBufferSource;
 | 
			
		||||
import net.minecraft.client.resources.model.BakedModel;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.network.protocol.Packet;
 | 
			
		||||
import net.minecraft.network.protocol.game.ServerGamePacketListener;
 | 
			
		||||
import net.minecraft.sounds.SoundEvent;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
@@ -18,11 +22,12 @@ public interface ClientPlatformHelper extends dan200.computercraft.impl.client.C
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send a network message to the server.
 | 
			
		||||
     * Convert a serverbound {@link NetworkMessage} to a Minecraft {@link Packet}.
 | 
			
		||||
     *
 | 
			
		||||
     * @param message The message to send.
 | 
			
		||||
     * @param message The messsge to convert.
 | 
			
		||||
     * @return The converted message.
 | 
			
		||||
     */
 | 
			
		||||
    void sendToServer(NetworkMessage<ServerNetworkContext> message);
 | 
			
		||||
    Packet<ServerGamePacketListener> createPacket(NetworkMessage<ServerNetworkContext> message);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Render a {@link BakedModel}, using any loader-specific hooks.
 | 
			
		||||
@@ -35,4 +40,13 @@ public interface ClientPlatformHelper extends dan200.computercraft.impl.client.C
 | 
			
		||||
     * @param tints         Block colour tints to apply to the model.
 | 
			
		||||
     */
 | 
			
		||||
    void renderBakedModel(PoseStack transform, MultiBufferSource buffers, BakedModel model, int lightmapCoord, int overlayLight, @Nullable int[] tints);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Play a record at a particular position.
 | 
			
		||||
     *
 | 
			
		||||
     * @param pos   The position to play this record.
 | 
			
		||||
     * @param sound The record to play, or {@code null} to stop it.
 | 
			
		||||
     * @see net.minecraft.client.renderer.LevelRenderer#playStreamingMusic(SoundEvent, BlockPos)
 | 
			
		||||
     */
 | 
			
		||||
    void playStreamingMusic(BlockPos pos, @Nullable SoundEvent sound);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ package dan200.computercraft.client.sound;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.audio.Channel;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral;
 | 
			
		||||
import io.netty.buffer.ByteBuf;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
 | 
			
		||||
import net.minecraft.client.sounds.AudioStream;
 | 
			
		||||
import net.minecraft.client.sounds.SoundEngine;
 | 
			
		||||
import org.lwjgl.BufferUtils;
 | 
			
		||||
@@ -36,7 +36,7 @@ class DfpwmStream implements AudioStream {
 | 
			
		||||
    /**
 | 
			
		||||
     * The {@link Channel} which this sound is playing on.
 | 
			
		||||
     *
 | 
			
		||||
     * @see SpeakerInstance#pushAudio(ByteBuf)
 | 
			
		||||
     * @see SpeakerInstance#playAudio(SpeakerPosition, float, ByteBuffer)
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    Channel channel;
 | 
			
		||||
@@ -44,7 +44,7 @@ class DfpwmStream implements AudioStream {
 | 
			
		||||
    /**
 | 
			
		||||
     * The underlying {@link SoundEngine} executor.
 | 
			
		||||
     *
 | 
			
		||||
     * @see SpeakerInstance#pushAudio(ByteBuf)
 | 
			
		||||
     * @see SpeakerInstance#playAudio(SpeakerPosition, float, ByteBuffer)
 | 
			
		||||
     * @see SoundEngine#executor
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
@@ -58,12 +58,12 @@ class DfpwmStream implements AudioStream {
 | 
			
		||||
    DfpwmStream() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void push(ByteBuf input) {
 | 
			
		||||
        var readable = input.readableBytes();
 | 
			
		||||
    void push(ByteBuffer input) {
 | 
			
		||||
        var readable = input.remaining();
 | 
			
		||||
        var output = ByteBuffer.allocate(readable * 8).order(ByteOrder.nativeOrder());
 | 
			
		||||
 | 
			
		||||
        for (var i = 0; i < readable; i++) {
 | 
			
		||||
            var inputByte = input.readByte();
 | 
			
		||||
            var inputByte = input.get();
 | 
			
		||||
            for (var j = 0; j < 8; j++) {
 | 
			
		||||
                var currentBit = (inputByte & 1) != 0;
 | 
			
		||||
                var target = currentBit ? 127 : -128;
 | 
			
		||||
 
 | 
			
		||||
@@ -7,11 +7,11 @@ package dan200.computercraft.client.sound;
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.core.util.Nullability;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
 | 
			
		||||
import io.netty.buffer.ByteBuf;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An instance of a speaker, which is either playing a {@link DfpwmStream} stream or a normal sound.
 | 
			
		||||
@@ -25,7 +25,7 @@ public class SpeakerInstance {
 | 
			
		||||
    SpeakerInstance() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public synchronized void pushAudio(ByteBuf buffer) {
 | 
			
		||||
    private void pushAudio(ByteBuffer buffer) {
 | 
			
		||||
        var sound = this.sound;
 | 
			
		||||
 | 
			
		||||
        var stream = currentStream;
 | 
			
		||||
@@ -43,7 +43,9 @@ public class SpeakerInstance {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void playAudio(SpeakerPosition position, float volume) {
 | 
			
		||||
    public void playAudio(SpeakerPosition position, float volume, ByteBuffer buffer) {
 | 
			
		||||
        pushAudio(buffer);
 | 
			
		||||
 | 
			
		||||
        var soundManager = Minecraft.getInstance().getSoundManager();
 | 
			
		||||
 | 
			
		||||
        if (sound != null && sound.stream != currentStream) {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,11 +11,14 @@ import dan200.computercraft.api.turtle.ITurtleAccess;
 | 
			
		||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
 | 
			
		||||
import dan200.computercraft.api.turtle.TurtleSide;
 | 
			
		||||
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
 | 
			
		||||
import dan200.computercraft.impl.PlatformHelper;
 | 
			
		||||
import dan200.computercraft.impl.TurtleUpgrades;
 | 
			
		||||
import dan200.computercraft.impl.UpgradeManager;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.nbt.CompoundTag;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.WeakHashMap;
 | 
			
		||||
@@ -24,14 +27,15 @@ import java.util.stream.Stream;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A registry of {@link TurtleUpgradeModeller}s.
 | 
			
		||||
 *
 | 
			
		||||
 * @see dan200.computercraft.api.client.ComputerCraftAPIClient#registerTurtleUpgradeModeller(TurtleUpgradeSerialiser, TurtleUpgradeModeller)
 | 
			
		||||
 */
 | 
			
		||||
public final class TurtleUpgradeModellers {
 | 
			
		||||
    private static final Logger LOG = LoggerFactory.getLogger(TurtleUpgradeModellers.class);
 | 
			
		||||
 | 
			
		||||
    private static final TurtleUpgradeModeller<ITurtleUpgrade> NULL_TURTLE_MODELLER = (upgrade, turtle, side) ->
 | 
			
		||||
        new TransformedModel(Minecraft.getInstance().getModelManager().getMissingModel(), Transformation.identity());
 | 
			
		||||
 | 
			
		||||
    private static final Map<TurtleUpgradeSerialiser<?>, TurtleUpgradeModeller<?>> turtleModels = new ConcurrentHashMap<>();
 | 
			
		||||
    private static volatile boolean fetchedModels;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * In order to avoid a double lookup of {@link ITurtleUpgrade} to {@link UpgradeManager.UpgradeWrapper} to
 | 
			
		||||
@@ -45,12 +49,18 @@ public final class TurtleUpgradeModellers {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static <T extends ITurtleUpgrade> void register(TurtleUpgradeSerialiser<T> serialiser, TurtleUpgradeModeller<T> modeller) {
 | 
			
		||||
        synchronized (turtleModels) {
 | 
			
		||||
            if (turtleModels.containsKey(serialiser)) {
 | 
			
		||||
                throw new IllegalStateException("Modeller already registered for serialiser");
 | 
			
		||||
        if (fetchedModels) {
 | 
			
		||||
            // TODO(1.20.4): Replace with an error.
 | 
			
		||||
            LOG.warn(
 | 
			
		||||
                "Turtle upgrade serialiser {} was registered too late, its models may not be loaded correctly. If you are " +
 | 
			
		||||
                    "the mod author, you may be using a deprecated API - see https://github.com/cc-tweaked/CC-Tweaked/pull/1684 " +
 | 
			
		||||
                    "for further information.",
 | 
			
		||||
                PlatformHelper.get().getRegistryKey(TurtleUpgradeSerialiser.registryId(), serialiser)
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
            turtleModels.put(serialiser, modeller);
 | 
			
		||||
        if (turtleModels.putIfAbsent(serialiser, modeller) != null) {
 | 
			
		||||
            throw new IllegalStateException("Modeller already registered for serialiser");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -75,6 +85,7 @@ public final class TurtleUpgradeModellers {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static Stream<ResourceLocation> getDependencies() {
 | 
			
		||||
        fetchedModels = true;
 | 
			
		||||
        return turtleModels.values().stream().flatMap(x -> x.getDependencies().stream());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
    "required": true,
 | 
			
		||||
    "package": "dan200.computercraft.mixin.client",
 | 
			
		||||
    "minVersion": "0.8",
 | 
			
		||||
    "compatibilityLevel": "JAVA_17",
 | 
			
		||||
    "injectors": {
 | 
			
		||||
        "defaultRequire": 1
 | 
			
		||||
    },
 | 
			
		||||
    "client": [
 | 
			
		||||
        "ClientPacketListenerMixin"
 | 
			
		||||
    ],
 | 
			
		||||
    "refmap": "client-computercraft.refmap.json"
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,48 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.data;
 | 
			
		||||
 | 
			
		||||
import com.google.common.hash.HashCode;
 | 
			
		||||
import com.google.common.hash.HashFunction;
 | 
			
		||||
import com.google.common.hash.Hashing;
 | 
			
		||||
import net.minecraft.data.CachedOutput;
 | 
			
		||||
import net.minecraft.data.DataProvider;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.nio.file.Path;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Wraps an existing {@link DataProvider}, passing generated JSON through {@link PrettyJsonWriter}.
 | 
			
		||||
 *
 | 
			
		||||
 * @param provider The provider to wrap.
 | 
			
		||||
 * @param <T>      The type of the provider to wrap.
 | 
			
		||||
 */
 | 
			
		||||
public record PrettyDataProvider<T extends DataProvider>(T provider) implements DataProvider {
 | 
			
		||||
    @Override
 | 
			
		||||
    public CompletableFuture<?> run(CachedOutput cachedOutput) {
 | 
			
		||||
        return provider.run(new Output(cachedOutput));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getName() {
 | 
			
		||||
        return provider.getName();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private record Output(CachedOutput output) implements CachedOutput {
 | 
			
		||||
        @SuppressWarnings("deprecation")
 | 
			
		||||
        private static final HashFunction HASH_FUNCTION = Hashing.sha1();
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void writeIfNeeded(Path path, byte[] bytes, HashCode hashCode) throws IOException {
 | 
			
		||||
            if (path.getFileName().toString().endsWith(".json")) {
 | 
			
		||||
                bytes = PrettyJsonWriter.reformat(bytes);
 | 
			
		||||
                hashCode = HASH_FUNCTION.hashBytes(bytes);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            output.writeIfNeeded(path, bytes, hashCode);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -22,11 +22,10 @@ import java.util.List;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Alternative version of {@link JsonWriter} which attempts to lay out the JSON in a more compact format.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Yes, this is at least a little deranged.
 | 
			
		||||
 *
 | 
			
		||||
 * @see PrettyDataProvider
 | 
			
		||||
 */
 | 
			
		||||
public class PrettyJsonWriter extends JsonWriter {
 | 
			
		||||
    public static final boolean ENABLED = System.getProperty("cct.pretty-json") != null;
 | 
			
		||||
    private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
 | 
			
		||||
 | 
			
		||||
    private static final int MAX_WIDTH = 120;
 | 
			
		||||
@@ -44,17 +43,6 @@ public class PrettyJsonWriter extends JsonWriter {
 | 
			
		||||
        this.out = out;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a JSON writer. This will either be a pretty or normal version, depending on whether the global flag is
 | 
			
		||||
     * set.
 | 
			
		||||
     *
 | 
			
		||||
     * @param out The writer to emit to.
 | 
			
		||||
     * @return The constructed JSON writer.
 | 
			
		||||
     */
 | 
			
		||||
    public static JsonWriter createWriter(Writer out) {
 | 
			
		||||
        return ENABLED ? new PrettyJsonWriter(out) : new JsonWriter(out);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reformat a JSON string with our pretty printer.
 | 
			
		||||
     *
 | 
			
		||||
@@ -62,8 +50,6 @@ public class PrettyJsonWriter extends JsonWriter {
 | 
			
		||||
     * @return The reformatted string.
 | 
			
		||||
     */
 | 
			
		||||
    public static byte[] reformat(byte[] contents) {
 | 
			
		||||
        if (!ENABLED) return contents;
 | 
			
		||||
 | 
			
		||||
        JsonElement object;
 | 
			
		||||
        try (var reader = new InputStreamReader(new ByteArrayInputStream(contents), StandardCharsets.UTF_8)) {
 | 
			
		||||
            object = GSON.fromJson(reader, JsonElement.class);
 | 
			
		||||
 
 | 
			
		||||
@@ -123,7 +123,7 @@ public class UpgradeManager<R extends UpgradeSerialiser<? extends T>, T extends
 | 
			
		||||
        // TODO: Can we track which mod this resource came from and use that instead? It's theoretically possible,
 | 
			
		||||
        //  but maybe not ideal for datapacks.
 | 
			
		||||
        var modId = id.getNamespace();
 | 
			
		||||
        if (modId.equals("minecraft") || modId.equals("")) modId = ComputerCraftAPI.MOD_ID;
 | 
			
		||||
        if (modId.equals("minecraft") || modId.isEmpty()) modId = ComputerCraftAPI.MOD_ID;
 | 
			
		||||
 | 
			
		||||
        var upgrade = serialiser.fromJson(id, root);
 | 
			
		||||
        if (!upgrade.getUpgradeID().equals(id)) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,25 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.mixin;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.data.PrettyJsonWriter;
 | 
			
		||||
import org.spongepowered.asm.mixin.Mixin;
 | 
			
		||||
import org.spongepowered.asm.mixin.injection.At;
 | 
			
		||||
import org.spongepowered.asm.mixin.injection.ModifyArg;
 | 
			
		||||
 | 
			
		||||
@Mixin(targets = "net/minecraft/data/HashCache$CacheUpdater")
 | 
			
		||||
public class CacheUpdaterMixin {
 | 
			
		||||
    @SuppressWarnings("UnusedMethod")
 | 
			
		||||
    @ModifyArg(
 | 
			
		||||
        method = "writeIfNeeded",
 | 
			
		||||
        at = @At(value = "INVOKE", target = "Ljava/nio/file/Files;write(Ljava/nio/file/Path;[B[Ljava/nio/file/OpenOption;)Ljava/nio/file/Path;"),
 | 
			
		||||
        require = 0
 | 
			
		||||
    )
 | 
			
		||||
    private byte[] reformatJson(byte[] contents) {
 | 
			
		||||
        // It would be cleaner to do this inside DataProvider.saveStable, but Forge's version of Mixin doesn't allow us
 | 
			
		||||
        // to inject into interfaces.
 | 
			
		||||
        return PrettyJsonWriter.reformat(contents);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -54,7 +54,12 @@ import static net.minecraft.commands.Commands.literal;
 | 
			
		||||
 | 
			
		||||
public final class CommandComputerCraft {
 | 
			
		||||
    public static final UUID SYSTEM_UUID = new UUID(0, 0);
 | 
			
		||||
    public static final String OPEN_COMPUTER = "computercraft open-computer ";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The client-side command to open the folder. Ideally this would live under the main {@code computercraft}
 | 
			
		||||
     * namespace, but unfortunately that overrides commands, rather than merging them.
 | 
			
		||||
     */
 | 
			
		||||
    public static final String CLIENT_OPEN_FOLDER = "computercraft-computer-folder";
 | 
			
		||||
 | 
			
		||||
    private CommandComputerCraft() {
 | 
			
		||||
    }
 | 
			
		||||
@@ -389,7 +394,7 @@ public final class CommandComputerCraft {
 | 
			
		||||
 | 
			
		||||
        return link(
 | 
			
		||||
            text("\u270E"),
 | 
			
		||||
            "/" + OPEN_COMPUTER + id,
 | 
			
		||||
            "/" + CLIENT_OPEN_FOLDER + " " + id,
 | 
			
		||||
            Component.translatable("commands.computercraft.dump.open_path")
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ package dan200.computercraft.shared.command.text;
 | 
			
		||||
import dan200.computercraft.core.util.Nullability;
 | 
			
		||||
import dan200.computercraft.shared.command.CommandUtils;
 | 
			
		||||
import dan200.computercraft.shared.network.client.ChatTableClientMessage;
 | 
			
		||||
import dan200.computercraft.shared.platform.PlatformHelper;
 | 
			
		||||
import dan200.computercraft.shared.network.server.ServerNetworking;
 | 
			
		||||
import net.minecraft.commands.CommandSourceStack;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.server.level.ServerPlayer;
 | 
			
		||||
@@ -105,7 +105,7 @@ public class TableBuilder {
 | 
			
		||||
        if (CommandUtils.isPlayer(source)) {
 | 
			
		||||
            trim(18);
 | 
			
		||||
            var player = (ServerPlayer) Nullability.assertNonNull(source.getEntity());
 | 
			
		||||
            PlatformHelper.get().sendToPlayer(new ChatTableClientMessage(this), player);
 | 
			
		||||
            ServerNetworking.sendToPlayer(new ChatTableClientMessage(this), player);
 | 
			
		||||
        } else {
 | 
			
		||||
            trim(100);
 | 
			
		||||
            new ServerTableFormatter(source).display(this);
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,6 @@ import net.minecraft.core.Direction;
 | 
			
		||||
import net.minecraft.nbt.CompoundTag;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
 | 
			
		||||
import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
import net.minecraft.world.*;
 | 
			
		||||
import net.minecraft.world.entity.player.Player;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
@@ -51,7 +50,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
 | 
			
		||||
    private boolean fresh = false;
 | 
			
		||||
 | 
			
		||||
    private int invalidSides = 0;
 | 
			
		||||
    private final ComponentAccess<IPeripheral> peripherals = PlatformHelper.get().createPeripheralAccess(d -> invalidSides |= 1 << d.ordinal());
 | 
			
		||||
    private final ComponentAccess<IPeripheral> peripherals = PlatformHelper.get().createPeripheralAccess(this, d -> invalidSides |= 1 << d.ordinal());
 | 
			
		||||
 | 
			
		||||
    private LockCode lockCode = LockCode.NO_LOCK;
 | 
			
		||||
 | 
			
		||||
@@ -218,7 +217,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
 | 
			
		||||
        var localDir = remapToLocalSide(dir);
 | 
			
		||||
        if (isPeripheralBlockedOnSide(localDir)) return;
 | 
			
		||||
 | 
			
		||||
        var peripheral = peripherals.get((ServerLevel) getLevel(), getBlockPos(), dir);
 | 
			
		||||
        var peripheral = peripherals.get(dir);
 | 
			
		||||
        computer.setPeripheral(localDir, peripheral);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -20,28 +20,24 @@ import net.minecraft.world.level.block.state.BlockState;
 | 
			
		||||
import net.minecraft.world.phys.Vec2;
 | 
			
		||||
import net.minecraft.world.phys.Vec3;
 | 
			
		||||
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
public class CommandComputerBlockEntity extends ComputerBlockEntity {
 | 
			
		||||
    public class CommandReceiver implements CommandSource {
 | 
			
		||||
        private final Map<Integer, String> output = new HashMap<>();
 | 
			
		||||
        private final List<String> output = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
        public void clearOutput() {
 | 
			
		||||
            output.clear();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Map<Integer, String> getOutput() {
 | 
			
		||||
            return output;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Map<Integer, String> copyOutput() {
 | 
			
		||||
            return new HashMap<>(output);
 | 
			
		||||
        public List<String> copyOutput() {
 | 
			
		||||
            return new ArrayList<>(output);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void sendSystemMessage(Component textComponent) {
 | 
			
		||||
            output.put(output.size() + 1, textComponent.getString());
 | 
			
		||||
            output.add(textComponent.getString());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ import dan200.computercraft.shared.config.Config;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.client.ClientNetworkContext;
 | 
			
		||||
import dan200.computercraft.shared.network.client.ComputerTerminalClientMessage;
 | 
			
		||||
import dan200.computercraft.shared.platform.PlatformHelper;
 | 
			
		||||
import dan200.computercraft.shared.network.server.ServerNetworking;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
import net.minecraft.world.inventory.AbstractContainerMenu;
 | 
			
		||||
@@ -142,7 +142,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
 | 
			
		||||
 | 
			
		||||
        for (var player : server.getPlayerList().getPlayers()) {
 | 
			
		||||
            if (player.containerMenu instanceof ComputerMenu && ((ComputerMenu) player.containerMenu).getComputer() == this) {
 | 
			
		||||
                PlatformHelper.get().sendToPlayer(createPacket.apply(player.containerMenu), player);
 | 
			
		||||
                ServerNetworking.sendToPlayer(createPacket.apply(player.containerMenu), player);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ import dan200.computercraft.shared.computer.upload.FileSlice;
 | 
			
		||||
import dan200.computercraft.shared.computer.upload.FileUpload;
 | 
			
		||||
import dan200.computercraft.shared.computer.upload.UploadResult;
 | 
			
		||||
import dan200.computercraft.shared.network.client.UploadResultMessage;
 | 
			
		||||
import dan200.computercraft.shared.platform.PlatformHelper;
 | 
			
		||||
import dan200.computercraft.shared.network.server.ServerNetworking;
 | 
			
		||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
 | 
			
		||||
import it.unimi.dsi.fastutil.ints.IntSet;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
@@ -138,7 +138,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        PlatformHelper.get().sendToPlayer(finishUpload(uploader), uploader);
 | 
			
		||||
        ServerNetworking.sendToPlayer(finishUpload(uploader), uploader);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private UploadResultMessage finishUpload(ServerPlayer player) {
 | 
			
		||||
@@ -159,7 +159,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
 | 
			
		||||
                toUpload.stream().map(x -> new TransferredFile(x.getName(), new ByteBufferChannel(x.getBytes()))).toList(),
 | 
			
		||||
                () -> {
 | 
			
		||||
                    if (player.isAlive() && player.containerMenu == owner) {
 | 
			
		||||
                        PlatformHelper.get().sendToPlayer(UploadResultMessage.consumed(owner), player);
 | 
			
		||||
                        ServerNetworking.sendToPlayer(UploadResultMessage.consumed(owner), player);
 | 
			
		||||
                    }
 | 
			
		||||
                }),
 | 
			
		||||
        });
 | 
			
		||||
 
 | 
			
		||||
@@ -224,7 +224,7 @@ public final class ConfigSpec {
 | 
			
		||||
                .defineInRange("max_requests", CoreConfig.httpMaxRequests, 0, Integer.MAX_VALUE);
 | 
			
		||||
 | 
			
		||||
            httpMaxWebsockets = builder
 | 
			
		||||
                .comment("The number of websockets a computer can have open at one time. Set to 0 for unlimited.")
 | 
			
		||||
                .comment("The number of websockets a computer can have open at one time.")
 | 
			
		||||
                .defineInRange("max_websockets", CoreConfig.httpMaxWebsockets, 1, Integer.MAX_VALUE);
 | 
			
		||||
 | 
			
		||||
            builder
 | 
			
		||||
 
 | 
			
		||||
@@ -4,30 +4,24 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.network.client;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.impl.Services;
 | 
			
		||||
import dan200.computercraft.shared.command.text.TableBuilder;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerState;
 | 
			
		||||
import dan200.computercraft.shared.computer.terminal.TerminalState;
 | 
			
		||||
import dan200.computercraft.shared.computer.upload.UploadResult;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
 | 
			
		||||
import io.netty.buffer.ByteBuf;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.sounds.SoundEvent;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The context under which clientbound packets are evaluated.
 | 
			
		||||
 */
 | 
			
		||||
public interface ClientNetworkContext {
 | 
			
		||||
    static ClientNetworkContext get() {
 | 
			
		||||
        var instance = Instance.INSTANCE;
 | 
			
		||||
        return instance == null ? Services.raise(ClientNetworkContext.class, Instance.ERROR) : instance;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void handleChatTable(TableBuilder table);
 | 
			
		||||
 | 
			
		||||
    void handleComputerTerminal(int containerId, TerminalState terminal);
 | 
			
		||||
@@ -40,9 +34,7 @@ public interface ClientNetworkContext {
 | 
			
		||||
 | 
			
		||||
    void handlePocketComputerDeleted(int instanceId);
 | 
			
		||||
 | 
			
		||||
    void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume);
 | 
			
		||||
 | 
			
		||||
    void handleSpeakerAudioPush(UUID source, ByteBuf buffer);
 | 
			
		||||
    void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, ByteBuffer audio);
 | 
			
		||||
 | 
			
		||||
    void handleSpeakerMove(UUID source, SpeakerPosition.Message position);
 | 
			
		||||
 | 
			
		||||
@@ -51,18 +43,4 @@ public interface ClientNetworkContext {
 | 
			
		||||
    void handleSpeakerStop(UUID source);
 | 
			
		||||
 | 
			
		||||
    void handleUploadResult(int containerId, UploadResult result, @Nullable Component errorMessage);
 | 
			
		||||
 | 
			
		||||
    final class Instance {
 | 
			
		||||
        static final @Nullable ClientNetworkContext INSTANCE;
 | 
			
		||||
        static final @Nullable Throwable ERROR;
 | 
			
		||||
 | 
			
		||||
        static {
 | 
			
		||||
            var helper = Services.tryLoad(ClientNetworkContext.class);
 | 
			
		||||
            INSTANCE = helper.instance();
 | 
			
		||||
            ERROR = helper.error();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Instance() {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,12 +11,9 @@ import dan200.computercraft.shared.peripheral.speaker.SpeakerBlockEntity;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.core.util.Nullability.assertNonNull;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Starts a sound on the client.
 | 
			
		||||
 * <p>
 | 
			
		||||
@@ -27,7 +24,7 @@ import static dan200.computercraft.core.util.Nullability.assertNonNull;
 | 
			
		||||
public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkContext> {
 | 
			
		||||
    private final UUID source;
 | 
			
		||||
    private final SpeakerPosition.Message pos;
 | 
			
		||||
    private final @Nullable ByteBuffer content;
 | 
			
		||||
    private final ByteBuffer content;
 | 
			
		||||
    private final float volume;
 | 
			
		||||
 | 
			
		||||
    public SpeakerAudioClientMessage(UUID source, SpeakerPosition pos, float volume, ByteBuffer content) {
 | 
			
		||||
@@ -42,10 +39,9 @@ public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkCo
 | 
			
		||||
        pos = SpeakerPosition.Message.read(buf);
 | 
			
		||||
        volume = buf.readFloat();
 | 
			
		||||
 | 
			
		||||
        // TODO: Remove this, so we no longer need a getter for ClientNetworkContext. However, doing so without
 | 
			
		||||
        //  leaking or redundantly copying the buffer is hard.
 | 
			
		||||
        ClientNetworkContext.get().handleSpeakerAudioPush(source, buf);
 | 
			
		||||
        content = null;
 | 
			
		||||
        var bytes = new byte[buf.readableBytes()];
 | 
			
		||||
        buf.readBytes(bytes);
 | 
			
		||||
        content = ByteBuffer.wrap(bytes);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -53,12 +49,12 @@ public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkCo
 | 
			
		||||
        buf.writeUUID(source);
 | 
			
		||||
        pos.write(buf);
 | 
			
		||||
        buf.writeFloat(volume);
 | 
			
		||||
        buf.writeBytes(assertNonNull(content).duplicate());
 | 
			
		||||
        buf.writeBytes(content.duplicate());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void handle(ClientNetworkContext context) {
 | 
			
		||||
        context.handleSpeakerAudio(source, pos, volume);
 | 
			
		||||
        context.handleSpeakerAudio(source, pos, volume, content);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,82 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.network.server;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.client.ClientNetworkContext;
 | 
			
		||||
import dan200.computercraft.shared.platform.PlatformHelper;
 | 
			
		||||
import net.minecraft.server.MinecraftServer;
 | 
			
		||||
import net.minecraft.server.level.ServerChunkCache;
 | 
			
		||||
import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
import net.minecraft.server.level.ServerPlayer;
 | 
			
		||||
import net.minecraft.world.level.chunk.LevelChunk;
 | 
			
		||||
import net.minecraft.world.phys.Vec3;
 | 
			
		||||
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Methods for sending network messages from the server to clients.
 | 
			
		||||
 */
 | 
			
		||||
public final class ServerNetworking {
 | 
			
		||||
    private ServerNetworking() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send a message to a specific player.
 | 
			
		||||
     *
 | 
			
		||||
     * @param message The message to send.
 | 
			
		||||
     * @param player  The player to send it to.
 | 
			
		||||
     */
 | 
			
		||||
    public static void sendToPlayer(NetworkMessage<ClientNetworkContext> message, ServerPlayer player) {
 | 
			
		||||
        player.connection.send(PlatformHelper.get().createPacket(message));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send a message to a set of players.
 | 
			
		||||
     *
 | 
			
		||||
     * @param message The message to send.
 | 
			
		||||
     * @param players The players to send it to.
 | 
			
		||||
     */
 | 
			
		||||
    public static void sendToPlayers(NetworkMessage<ClientNetworkContext> message, Collection<ServerPlayer> players) {
 | 
			
		||||
        if (players.isEmpty()) return;
 | 
			
		||||
        var packet = PlatformHelper.get().createPacket(message);
 | 
			
		||||
        for (var player : players) player.connection.send(packet);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send a message to all players.
 | 
			
		||||
     *
 | 
			
		||||
     * @param message The message to send.
 | 
			
		||||
     * @param server  The current server.
 | 
			
		||||
     */
 | 
			
		||||
    public static void sendToAllPlayers(NetworkMessage<ClientNetworkContext> message, MinecraftServer server) {
 | 
			
		||||
        server.getPlayerList().broadcastAll(PlatformHelper.get().createPacket(message));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send a message to all players around a point.
 | 
			
		||||
     *
 | 
			
		||||
     * @param message  The message to send.
 | 
			
		||||
     * @param level    The level the point is in.
 | 
			
		||||
     * @param pos      The centre position.
 | 
			
		||||
     * @param distance The distance to the centre players must be within.
 | 
			
		||||
     */
 | 
			
		||||
    public static void sendToAllAround(NetworkMessage<ClientNetworkContext> message, ServerLevel level, Vec3 pos, float distance) {
 | 
			
		||||
        level.getServer().getPlayerList().broadcast(null, pos.x, pos.y, pos.z, distance, level.dimension(), PlatformHelper.get().createPacket(message));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send a message to all players tracking a chunk.
 | 
			
		||||
     *
 | 
			
		||||
     * @param message The message to send.
 | 
			
		||||
     * @param chunk   The chunk players must be tracking.
 | 
			
		||||
     */
 | 
			
		||||
    public static void sendToAllTracking(NetworkMessage<ClientNetworkContext> message, LevelChunk chunk) {
 | 
			
		||||
        var packet = PlatformHelper.get().createPacket(message);
 | 
			
		||||
        for (var player : ((ServerChunkCache) chunk.getLevel().getChunkSource()).chunkMap.getPlayers(chunk.getPos(), false)) {
 | 
			
		||||
            player.connection.send(packet);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -7,11 +7,12 @@ package dan200.computercraft.shared.peripheral.diskdrive;
 | 
			
		||||
import com.google.errorprone.annotations.concurrent.GuardedBy;
 | 
			
		||||
import dan200.computercraft.api.filesystem.Mount;
 | 
			
		||||
import dan200.computercraft.api.filesystem.WritableMount;
 | 
			
		||||
import dan200.computercraft.api.media.IMedia;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IComputerAccess;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import dan200.computercraft.shared.common.AbstractContainerBlockEntity;
 | 
			
		||||
import dan200.computercraft.shared.network.client.PlayRecordClientMessage;
 | 
			
		||||
import dan200.computercraft.shared.platform.PlatformHelper;
 | 
			
		||||
import dan200.computercraft.shared.network.server.ServerNetworking;
 | 
			
		||||
import dan200.computercraft.shared.util.WorldUtil;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.core.Direction;
 | 
			
		||||
@@ -32,6 +33,28 @@ import java.util.Map;
 | 
			
		||||
import java.util.concurrent.atomic.AtomicBoolean;
 | 
			
		||||
import java.util.concurrent.atomic.AtomicReference;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The underlying block entity for disk drives. This holds the main logic for the {@linkplain DiskDrivePeripheral disk
 | 
			
		||||
 * drive peripheral}, such as handling mounts and {@linkplain DiskDrivePeripheral#playAudio() playing audio}.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Most disk drive peripheral methods execute on the computer thread (largely due to historic reasons). This causes some
 | 
			
		||||
 * problems, as the disk item could be read by both the computer thread (via peripheral calls) and main thread (via
 | 
			
		||||
 * Minecraft inventory interaction).
 | 
			
		||||
 * <p>
 | 
			
		||||
 * To solve this, we use an immutable {@link MediaStack}, which holds an immutable version of the current
 | 
			
		||||
 * {@link ItemStack} (and its corresponding {@link IMedia}). When the {@linkplain #setChanged() inventory is changed},
 | 
			
		||||
 * we {@linkplain #updateMedia() update the media stack} and recompute mounts.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This is somewhat complicated by {@link #attach(IComputerAccess)}. As that can happen on the computer thread and
 | 
			
		||||
 * may mutate the stack (when {@link IMedia#createDataMount(ItemStack, ServerLevel)} assigns an ID for the first time),
 | 
			
		||||
 * we need a way to safely update the inventory. To solve this, all internal non-inventory interactions with disk drives
 | 
			
		||||
 * treat the media stack as the "primary" stack. This allows us to atomically update it, and then sync it back to the
 | 
			
		||||
 * main inventory ({@link #updateMediaStack(ItemStack, boolean)}) either directly ({@link #updateDiskFromMedia()}) or
 | 
			
		||||
 * on the next block tick ({@link #stackDirty}). This does mean there's a one-tick delay where the inventory may be
 | 
			
		||||
 * out-of-date, but that should happen very rarely.
 | 
			
		||||
 *
 | 
			
		||||
 * @see DiskDrivePeripheral
 | 
			
		||||
 */
 | 
			
		||||
public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
 | 
			
		||||
    private static final String NBT_ITEM = "Item";
 | 
			
		||||
 | 
			
		||||
@@ -42,11 +65,13 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
 | 
			
		||||
 | 
			
		||||
    private final DiskDrivePeripheral peripheral = new DiskDrivePeripheral(this);
 | 
			
		||||
 | 
			
		||||
    private final @GuardedBy("this") Map<IComputerAccess, MountInfo> computers = new HashMap<>();
 | 
			
		||||
 | 
			
		||||
    private final NonNullList<ItemStack> inventory = NonNullList.withSize(1, ItemStack.EMPTY);
 | 
			
		||||
 | 
			
		||||
    @GuardedBy("this")
 | 
			
		||||
    private final Map<IComputerAccess, MountInfo> computers = new HashMap<>();
 | 
			
		||||
    @GuardedBy("this")
 | 
			
		||||
    private MediaStack media = MediaStack.EMPTY;
 | 
			
		||||
    @GuardedBy("this")
 | 
			
		||||
    private @Nullable Mount mount;
 | 
			
		||||
 | 
			
		||||
    private boolean recordPlaying = false;
 | 
			
		||||
@@ -54,7 +79,12 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
 | 
			
		||||
    // then read them when ticking.
 | 
			
		||||
    private final AtomicReference<RecordCommand> recordQueued = new AtomicReference<>(null);
 | 
			
		||||
    private final AtomicBoolean ejectQueued = new AtomicBoolean(false);
 | 
			
		||||
    private final AtomicBoolean mountQueued = new AtomicBoolean(false);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether the stack in {@link #media} has been modified on the computer thread, and needs to be written back to the
 | 
			
		||||
     * inventory on the main thread.
 | 
			
		||||
     */
 | 
			
		||||
    private final AtomicBoolean stackDirty = new AtomicBoolean(false);
 | 
			
		||||
 | 
			
		||||
    public DiskDriveBlockEntity(BlockEntityType<DiskDriveBlockEntity> type, BlockPos pos, BlockState state) {
 | 
			
		||||
        super(type, pos, state);
 | 
			
		||||
@@ -66,7 +96,7 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void clearRemoved() {
 | 
			
		||||
        updateItem();
 | 
			
		||||
        updateMedia();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -93,12 +123,14 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void serverTick() {
 | 
			
		||||
        if (stackDirty.getAndSet(false)) updateDiskFromMedia();
 | 
			
		||||
        if (ejectQueued.getAndSet(false)) ejectContents();
 | 
			
		||||
 | 
			
		||||
        var recordQueued = this.recordQueued.getAndSet(null);
 | 
			
		||||
        if (recordQueued != null) {
 | 
			
		||||
            switch (recordQueued) {
 | 
			
		||||
                case PLAY -> {
 | 
			
		||||
                    var media = getMedia();
 | 
			
		||||
                    var record = media.getAudio();
 | 
			
		||||
                    if (record != null) {
 | 
			
		||||
                        recordPlaying = true;
 | 
			
		||||
@@ -112,12 +144,6 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (mountQueued.get()) {
 | 
			
		||||
            synchronized (this) {
 | 
			
		||||
                mountAll();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -127,25 +153,27 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setChanged() {
 | 
			
		||||
        if (level != null && !level.isClientSide) updateItem();
 | 
			
		||||
        if (level != null && !level.isClientSide) updateMedia();
 | 
			
		||||
        super.setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void updateItem() {
 | 
			
		||||
        var newDisk = getDiskStack();
 | 
			
		||||
        if (ItemStack.isSameItemSameTags(newDisk, media.stack)) return;
 | 
			
		||||
    /**
 | 
			
		||||
     * Called on the server after the item has changed. This unmounts the old media and mounts the new one.
 | 
			
		||||
     */
 | 
			
		||||
    private synchronized void updateMedia() {
 | 
			
		||||
        var newStack = getDiskStack();
 | 
			
		||||
        if (ItemStack.isSameItemSameTags(newStack, media.stack())) return;
 | 
			
		||||
 | 
			
		||||
        var media = MediaStack.of(newDisk);
 | 
			
		||||
        var newMedia = MediaStack.of(newStack);
 | 
			
		||||
 | 
			
		||||
        if (newDisk.isEmpty()) {
 | 
			
		||||
        if (newStack.isEmpty()) {
 | 
			
		||||
            updateBlockState(DiskDriveState.EMPTY);
 | 
			
		||||
        } else {
 | 
			
		||||
            updateBlockState(media.media != null ? DiskDriveState.FULL : DiskDriveState.INVALID);
 | 
			
		||||
            updateBlockState(newMedia.media() != null ? DiskDriveState.FULL : DiskDriveState.INVALID);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        synchronized (this) {
 | 
			
		||||
        // Unmount old disk
 | 
			
		||||
            if (!this.media.stack.isEmpty()) {
 | 
			
		||||
        if (!media.stack().isEmpty()) {
 | 
			
		||||
            for (var computer : computers.entrySet()) unmountDisk(computer.getKey(), computer.getValue());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -155,10 +183,16 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
 | 
			
		||||
            recordPlaying = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Use our new media, and (if needed) mount the new disk.
 | 
			
		||||
        mount = null;
 | 
			
		||||
            this.media = media;
 | 
			
		||||
        media = newMedia;
 | 
			
		||||
        stackDirty.set(false);
 | 
			
		||||
 | 
			
		||||
            mountAll();
 | 
			
		||||
        if (!newStack.isEmpty() && !computers.isEmpty()) {
 | 
			
		||||
            var mount = getOrCreateMount(true);
 | 
			
		||||
            for (var entry : computers.entrySet()) {
 | 
			
		||||
                mountDisk(entry.getKey(), entry.getValue(), mount);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -166,7 +200,7 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
 | 
			
		||||
        return getItem(0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    MediaStack getMedia() {
 | 
			
		||||
    synchronized MediaStack getMedia() {
 | 
			
		||||
        return media;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -181,17 +215,32 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the current disk stack, assuming the underlying item does not change. Unlike
 | 
			
		||||
     * {@link #setDiskStack(ItemStack)} this will not change any mounts.
 | 
			
		||||
     *
 | 
			
		||||
     * @param stack The new disk stack.
 | 
			
		||||
     * Update the inventory's disk stack from the media stack. Unlike {@link #setDiskStack(ItemStack)} this will not
 | 
			
		||||
     * change any mounts.
 | 
			
		||||
     */
 | 
			
		||||
    void updateDiskStack(ItemStack stack) {
 | 
			
		||||
        setItem(0, stack);
 | 
			
		||||
        if (!ItemStack.isSameItemSameTags(stack, media.stack)) {
 | 
			
		||||
            media = MediaStack.of(stack);
 | 
			
		||||
    private synchronized void updateDiskFromMedia() {
 | 
			
		||||
        // Write back the item to the main inventory, and then mark it as dirty.
 | 
			
		||||
        setItem(0, media.stack().copy());
 | 
			
		||||
        super.setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Atomically update {@link #media}'s stack, then sync it back to the main inventory.
 | 
			
		||||
     *
 | 
			
		||||
     * @param stack     The original stack.
 | 
			
		||||
     * @param immediate Whether to do this immediately (when called from the main thread) or asynchronously (when called
 | 
			
		||||
     *                  from the computer thread).
 | 
			
		||||
     */
 | 
			
		||||
    @GuardedBy("this")
 | 
			
		||||
    private void updateMediaStack(ItemStack stack, boolean immediate) {
 | 
			
		||||
        if (ItemStack.isSameItemSameTags(media.stack(), stack)) return;
 | 
			
		||||
        media = new MediaStack(stack, media.media());
 | 
			
		||||
 | 
			
		||||
        if (immediate) {
 | 
			
		||||
            updateDiskFromMedia();
 | 
			
		||||
        } else {
 | 
			
		||||
            stackDirty.set(true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
@@ -212,7 +261,9 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
 | 
			
		||||
        synchronized (this) {
 | 
			
		||||
            var info = new MountInfo();
 | 
			
		||||
            computers.put(computer, info);
 | 
			
		||||
            mountQueued.set(true);
 | 
			
		||||
            if (!media.stack().isEmpty()) {
 | 
			
		||||
                mountDisk(computer, info, getOrCreateMount(level instanceof ServerLevel l && l.getServer().isSameThread()));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -234,35 +285,31 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
 | 
			
		||||
        ejectQueued.set(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add our mount to all computers.
 | 
			
		||||
     */
 | 
			
		||||
    @GuardedBy("this")
 | 
			
		||||
    private void mountAll() {
 | 
			
		||||
        doMountAll();
 | 
			
		||||
        mountQueued.set(false);
 | 
			
		||||
    synchronized MountResult setDiskLabel(@Nullable String label) {
 | 
			
		||||
        if (media.media() == null) return MountResult.NO_MEDIA;
 | 
			
		||||
 | 
			
		||||
        // Set the label, and write it back to the media stack.
 | 
			
		||||
        var stack = media.stack().copy();
 | 
			
		||||
        if (!media.media().setLabel(stack, label)) return MountResult.NOT_ALLOWED;
 | 
			
		||||
        updateMediaStack(stack, true);
 | 
			
		||||
 | 
			
		||||
        return MountResult.CHANGED;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The worker for {@link #mountAll()}. This is responsible for creating the mount and placing it on all computers.
 | 
			
		||||
     */
 | 
			
		||||
    @GuardedBy("this")
 | 
			
		||||
    private void doMountAll() {
 | 
			
		||||
        if (computers.isEmpty() || media.media == null) return;
 | 
			
		||||
    private @Nullable Mount getOrCreateMount(boolean immediate) {
 | 
			
		||||
        if (media.media() == null) return null;
 | 
			
		||||
        if (mount != null) return mount;
 | 
			
		||||
 | 
			
		||||
        if (mount == null) {
 | 
			
		||||
            var stack = getDiskStack();
 | 
			
		||||
            mount = media.media.createDataMount(stack, (ServerLevel) level);
 | 
			
		||||
            setDiskStack(stack);
 | 
			
		||||
        // Set the id (if needed) and write it back to the media stack.
 | 
			
		||||
        var stack = media.stack().copy();
 | 
			
		||||
        mount = media.media().createDataMount(stack, (ServerLevel) level);
 | 
			
		||||
        updateMediaStack(stack, immediate);
 | 
			
		||||
 | 
			
		||||
        return mount;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        if (mount == null) return;
 | 
			
		||||
 | 
			
		||||
        for (var entry : computers.entrySet()) {
 | 
			
		||||
            var computer = entry.getKey();
 | 
			
		||||
            var info = entry.getValue();
 | 
			
		||||
            if (info.mountPath != null) continue;
 | 
			
		||||
 | 
			
		||||
    private static void mountDisk(IComputerAccess computer, MountInfo info, @Nullable Mount mount) {
 | 
			
		||||
        if (mount instanceof WritableMount writable) {
 | 
			
		||||
            // Try mounting at the lowest numbered "disk" name we can
 | 
			
		||||
            var n = 1;
 | 
			
		||||
@@ -270,18 +317,19 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
 | 
			
		||||
                info.mountPath = computer.mountWritable(n == 1 ? "disk" : "disk" + n, writable);
 | 
			
		||||
                n++;
 | 
			
		||||
            }
 | 
			
		||||
            } else {
 | 
			
		||||
        } else if (mount != null) {
 | 
			
		||||
            // Try mounting at the lowest numbered "disk" name we can
 | 
			
		||||
            var n = 1;
 | 
			
		||||
            while (info.mountPath == null) {
 | 
			
		||||
                info.mountPath = computer.mount(n == 1 ? "disk" : "disk" + n, mount);
 | 
			
		||||
                n++;
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            assert info.mountPath == null : "Mount path should be null";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        computer.queueEvent("disk", computer.getAttachmentName());
 | 
			
		||||
    }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void unmountDisk(IComputerAccess computer, MountInfo info) {
 | 
			
		||||
        if (info.mountPath != null) {
 | 
			
		||||
@@ -315,7 +363,7 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void sendMessage(PlayRecordClientMessage message) {
 | 
			
		||||
        PlatformHelper.get().sendToAllAround(message, (ServerLevel) getLevel(), Vec3.atCenterOf(getBlockPos()), 64);
 | 
			
		||||
        ServerNetworking.sendToAllAround(message, (ServerLevel) getLevel(), Vec3.atCenterOf(getBlockPos()), 64);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -327,4 +375,10 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
 | 
			
		||||
        PLAY,
 | 
			
		||||
        STOP,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    enum MountResult {
 | 
			
		||||
        NO_MEDIA,
 | 
			
		||||
        NOT_ALLOWED,
 | 
			
		||||
        CHANGED,
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,7 @@ public class DiskDrivePeripheral implements IPeripheral {
 | 
			
		||||
     */
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final boolean isDiskPresent() {
 | 
			
		||||
        return !diskDrive.getMedia().stack.isEmpty();
 | 
			
		||||
        return !diskDrive.getMedia().stack().isEmpty();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -64,7 +64,7 @@ public class DiskDrivePeripheral implements IPeripheral {
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final Object[] getDiskLabel() {
 | 
			
		||||
        var media = diskDrive.getMedia();
 | 
			
		||||
        return media.media == null ? null : new Object[]{ media.media.getLabel(media.stack) };
 | 
			
		||||
        return media.media() == null ? null : new Object[]{ media.media().getLabel(media.stack()) };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -80,15 +80,11 @@ public class DiskDrivePeripheral implements IPeripheral {
 | 
			
		||||
     */
 | 
			
		||||
    @LuaFunction(mainThread = true)
 | 
			
		||||
    public final void setDiskLabel(Optional<String> label) throws LuaException {
 | 
			
		||||
        var media = diskDrive.getMedia();
 | 
			
		||||
        if (media.media == null) return;
 | 
			
		||||
 | 
			
		||||
        // We're on the main thread so the stack and media should be in sync.
 | 
			
		||||
        var stack = diskDrive.getDiskStack();
 | 
			
		||||
        if (!media.media.setLabel(stack, label.map(StringUtil::normaliseLabel).orElse(null))) {
 | 
			
		||||
            throw new LuaException("Disk label cannot be changed");
 | 
			
		||||
        switch (diskDrive.setDiskLabel(label.map(StringUtil::normaliseLabel).orElse(null))) {
 | 
			
		||||
            case NOT_ALLOWED -> throw new LuaException("Disk label cannot be changed");
 | 
			
		||||
            case CHANGED, NO_MEDIA -> {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        diskDrive.updateDiskStack(stack);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -172,7 +168,7 @@ public class DiskDrivePeripheral implements IPeripheral {
 | 
			
		||||
    @Nullable
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final Object[] getDiskID() {
 | 
			
		||||
        var disk = diskDrive.getMedia().stack;
 | 
			
		||||
        var disk = diskDrive.getMedia().stack();
 | 
			
		||||
        return disk.getItem() instanceof DiskItem ? new Object[]{ DiskItem.getDiskID(disk) } : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,19 +13,14 @@ import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An immutable snapshot of the current disk. This allows us to read the stack in a thread-safe manner.
 | 
			
		||||
 *
 | 
			
		||||
 * @param stack An immutable {@link ItemStack}.
 | 
			
		||||
 * @param media The associated {@link IMedia} instance for this stack.
 | 
			
		||||
 */
 | 
			
		||||
final class MediaStack {
 | 
			
		||||
record MediaStack(ItemStack stack, @Nullable IMedia media) {
 | 
			
		||||
    static final MediaStack EMPTY = new MediaStack(ItemStack.EMPTY, null);
 | 
			
		||||
 | 
			
		||||
    final ItemStack stack;
 | 
			
		||||
    final @Nullable IMedia media;
 | 
			
		||||
 | 
			
		||||
    private MediaStack(ItemStack stack, @Nullable IMedia media) {
 | 
			
		||||
        this.stack = stack;
 | 
			
		||||
        this.media = media;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static MediaStack of(ItemStack stack) {
 | 
			
		||||
    static MediaStack of(ItemStack stack) {
 | 
			
		||||
        if (stack.isEmpty()) return EMPTY;
 | 
			
		||||
 | 
			
		||||
        var freshStack = stack.copy();
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ package dan200.computercraft.shared.peripheral.generic;
 | 
			
		||||
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.core.Direction;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntity;
 | 
			
		||||
import net.minecraft.world.level.block.state.BlockState;
 | 
			
		||||
 | 
			
		||||
@@ -32,5 +32,5 @@ public interface ComponentLookup<C extends Runnable> {
 | 
			
		||||
     * @return The found component, or {@code null} if not present.
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    Object find(Level level, BlockPos pos, BlockState state, BlockEntity blockEntity, Direction side, C invalidate);
 | 
			
		||||
    Object find(ServerLevel level, BlockPos pos, BlockState state, BlockEntity blockEntity, Direction side, C invalidate);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ import dan200.computercraft.core.methods.PeripheralMethod;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ServerContext;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.core.Direction;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntity;
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
@@ -43,7 +43,7 @@ public final class GenericPeripheralProvider<C extends Runnable> {
 | 
			
		||||
        if (!lookups.contains(lookup)) lookups.add(lookup);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void forEachMethod(MethodSupplier<PeripheralMethod> methods, Level level, BlockPos pos, Direction side, BlockEntity blockEntity, C invalidate, MethodSupplier.TargetedConsumer<PeripheralMethod> consumer) {
 | 
			
		||||
    public void forEachMethod(MethodSupplier<PeripheralMethod> methods, ServerLevel level, BlockPos pos, Direction side, BlockEntity blockEntity, C invalidate, MethodSupplier.TargetedConsumer<PeripheralMethod> consumer) {
 | 
			
		||||
        methods.forEachMethod(blockEntity, consumer);
 | 
			
		||||
 | 
			
		||||
        for (var lookup : lookups) {
 | 
			
		||||
@@ -53,17 +53,11 @@ public final class GenericPeripheralProvider<C extends Runnable> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public IPeripheral getPeripheral(Level level, BlockPos pos, Direction side, @Nullable BlockEntity blockEntity, C invalidate) {
 | 
			
		||||
    public IPeripheral getPeripheral(ServerLevel level, BlockPos pos, Direction side, @Nullable BlockEntity blockEntity, C invalidate) {
 | 
			
		||||
        if (blockEntity == null) return null;
 | 
			
		||||
 | 
			
		||||
        var server = level.getServer();
 | 
			
		||||
        if (server == null) {
 | 
			
		||||
            LOG.warn("Fetching peripherals on a non-server level {}.", level, new IllegalStateException("Fetching peripherals on a non-server level."));
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var builder = new GenericPeripheralBuilder();
 | 
			
		||||
        forEachMethod(ServerContext.get(server).peripheralMethods(), level, pos, side, blockEntity, invalidate, builder::addMethod);
 | 
			
		||||
        forEachMethod(ServerContext.get(level.getServer()).peripheralMethods(), level, pos, side, blockEntity, invalidate, builder::addMethod);
 | 
			
		||||
        return builder.toPeripheral(blockEntity, side);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,6 @@ import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.core.Direction;
 | 
			
		||||
import net.minecraft.nbt.CompoundTag;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
import net.minecraft.world.InteractionResult;
 | 
			
		||||
import net.minecraft.world.entity.player.Player;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
@@ -60,10 +59,11 @@ public class CableBlockEntity extends BlockEntity {
 | 
			
		||||
 | 
			
		||||
    private boolean invalidPeripheral;
 | 
			
		||||
    private boolean peripheralAccessAllowed;
 | 
			
		||||
    private final WiredModemLocalPeripheral peripheral = new WiredModemLocalPeripheral(this::queueRefreshPeripheral);
 | 
			
		||||
    private final WiredModemLocalPeripheral peripheral = new WiredModemLocalPeripheral(PlatformHelper.get().createPeripheralAccess(this, x -> queueRefreshPeripheral()));
 | 
			
		||||
    private @Nullable Runnable modemChanged;
 | 
			
		||||
 | 
			
		||||
    private boolean connectionsFormed = false;
 | 
			
		||||
    private boolean connectionsChanged = false;
 | 
			
		||||
 | 
			
		||||
    private final WiredModemElement cable = new CableElement();
 | 
			
		||||
    private final WiredNode node = cable.getNode();
 | 
			
		||||
@@ -88,7 +88,7 @@ public class CableBlockEntity extends BlockEntity {
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private final ComponentAccess<WiredElement> connectedElements = PlatformHelper.get().createWiredElementAccess(x -> connectionsChanged());
 | 
			
		||||
    private final ComponentAccess<WiredElement> connectedElements = PlatformHelper.get().createWiredElementAccess(this, x -> scheduleConnectionsChanged());
 | 
			
		||||
 | 
			
		||||
    public CableBlockEntity(BlockEntityType<? extends CableBlockEntity> type, BlockPos pos, BlockState state) {
 | 
			
		||||
        super(type, pos, state);
 | 
			
		||||
@@ -237,10 +237,18 @@ public class CableBlockEntity extends BlockEntity {
 | 
			
		||||
                updateConnectedPeripherals();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (connectionsChanged) connectionsChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void scheduleConnectionsChanged() {
 | 
			
		||||
        connectionsChanged = true;
 | 
			
		||||
        TickScheduler.schedule(tickToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void connectionsChanged() {
 | 
			
		||||
        if (getLevel().isClientSide) return;
 | 
			
		||||
        connectionsChanged = false;
 | 
			
		||||
 | 
			
		||||
        var state = getBlockState();
 | 
			
		||||
        var world = getLevel();
 | 
			
		||||
@@ -249,7 +257,7 @@ public class CableBlockEntity extends BlockEntity {
 | 
			
		||||
            var offset = current.relative(facing);
 | 
			
		||||
            if (!world.isLoaded(offset)) continue;
 | 
			
		||||
 | 
			
		||||
            var element = connectedElements.get((ServerLevel) world, current, facing);
 | 
			
		||||
            var element = connectedElements.get(facing);
 | 
			
		||||
            if (element == null) continue;
 | 
			
		||||
 | 
			
		||||
            var node = element.getNode();
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,6 @@ import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.core.Direction;
 | 
			
		||||
import net.minecraft.nbt.CompoundTag;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
import net.minecraft.world.InteractionResult;
 | 
			
		||||
import net.minecraft.world.entity.player.Player;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
@@ -75,21 +74,22 @@ public class WiredModemFullBlockEntity extends BlockEntity {
 | 
			
		||||
    private final WiredModemLocalPeripheral[] peripherals = new WiredModemLocalPeripheral[6];
 | 
			
		||||
 | 
			
		||||
    private boolean connectionsFormed = false;
 | 
			
		||||
    private boolean connectionsChanged = false;
 | 
			
		||||
 | 
			
		||||
    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 WiredNode node = element.getNode();
 | 
			
		||||
 | 
			
		||||
    private final ComponentAccess<WiredElement> connectedElements = PlatformHelper.get().createWiredElementAccess(x -> connectionsChanged());
 | 
			
		||||
    private final ComponentAccess<WiredElement> connectedElements = PlatformHelper.get().createWiredElementAccess(this, x -> scheduleConnectionsChanged());
 | 
			
		||||
 | 
			
		||||
    private int invalidSides = 0;
 | 
			
		||||
 | 
			
		||||
    public WiredModemFullBlockEntity(BlockEntityType<WiredModemFullBlockEntity> type, BlockPos pos, BlockState state) {
 | 
			
		||||
        super(type, pos, state);
 | 
			
		||||
        var peripheralAccess = PlatformHelper.get().createPeripheralAccess(this, this::queueRefreshPeripheral);
 | 
			
		||||
        for (var i = 0; i < peripherals.length; i++) {
 | 
			
		||||
            var facing = Direction.from3DDataValue(i);
 | 
			
		||||
            peripherals[i] = new WiredModemLocalPeripheral(() -> queueRefreshPeripheral(facing));
 | 
			
		||||
            peripherals[i] = new WiredModemLocalPeripheral(peripheralAccess);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -205,10 +205,18 @@ public class WiredModemFullBlockEntity extends BlockEntity {
 | 
			
		||||
                updateConnectedPeripherals();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (connectionsChanged) connectionsChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void scheduleConnectionsChanged() {
 | 
			
		||||
        connectionsChanged = true;
 | 
			
		||||
        TickScheduler.schedule(tickToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void connectionsChanged() {
 | 
			
		||||
        if (getLevel().isClientSide) return;
 | 
			
		||||
        connectionsChanged = false;
 | 
			
		||||
 | 
			
		||||
        var world = getLevel();
 | 
			
		||||
        var current = getBlockPos();
 | 
			
		||||
@@ -216,7 +224,7 @@ public class WiredModemFullBlockEntity extends BlockEntity {
 | 
			
		||||
            var offset = current.relative(facing);
 | 
			
		||||
            if (!world.isLoaded(offset)) continue;
 | 
			
		||||
 | 
			
		||||
            var element = connectedElements.get((ServerLevel) getLevel(), getBlockPos(), facing);
 | 
			
		||||
            var element = connectedElements.get(facing);
 | 
			
		||||
            if (element == null) continue;
 | 
			
		||||
 | 
			
		||||
            node.connectTo(element.getNode());
 | 
			
		||||
 
 | 
			
		||||
@@ -8,12 +8,10 @@ import dan200.computercraft.api.ComputerCraftTags;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ServerContext;
 | 
			
		||||
import dan200.computercraft.shared.platform.ComponentAccess;
 | 
			
		||||
import dan200.computercraft.shared.platform.PlatformHelper;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.core.Direction;
 | 
			
		||||
import net.minecraft.nbt.CompoundTag;
 | 
			
		||||
import net.minecraft.nbt.Tag;
 | 
			
		||||
import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
@@ -38,8 +36,8 @@ public final class WiredModemLocalPeripheral {
 | 
			
		||||
    private @Nullable IPeripheral peripheral;
 | 
			
		||||
    private final ComponentAccess<IPeripheral> peripherals;
 | 
			
		||||
 | 
			
		||||
    public WiredModemLocalPeripheral(Runnable invalidate) {
 | 
			
		||||
        peripherals = PlatformHelper.get().createPeripheralAccess(x -> invalidate.run());
 | 
			
		||||
    public WiredModemLocalPeripheral(ComponentAccess<IPeripheral> peripherals) {
 | 
			
		||||
        this.peripherals = peripherals;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -126,7 +124,7 @@ public final class WiredModemLocalPeripheral {
 | 
			
		||||
 | 
			
		||||
        if (world.getBlockState(offset).is(ComputerCraftTags.Blocks.PERIPHERAL_HUB_IGNORE)) return null;
 | 
			
		||||
 | 
			
		||||
        var peripheral = peripherals.get((ServerLevel) world, pos, direction);
 | 
			
		||||
        var peripheral = peripherals.get(direction);
 | 
			
		||||
        return peripheral instanceof WiredModemPeripheral ? null : peripheral;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ package dan200.computercraft.shared.peripheral.monitor;
 | 
			
		||||
import dan200.computercraft.shared.computer.terminal.TerminalState;
 | 
			
		||||
import dan200.computercraft.shared.config.Config;
 | 
			
		||||
import dan200.computercraft.shared.network.client.MonitorClientMessage;
 | 
			
		||||
import dan200.computercraft.shared.platform.PlatformHelper;
 | 
			
		||||
import dan200.computercraft.shared.network.server.ServerNetworking;
 | 
			
		||||
import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
import net.minecraft.server.level.ServerPlayer;
 | 
			
		||||
import net.minecraft.world.level.chunk.LevelChunk;
 | 
			
		||||
@@ -40,7 +40,7 @@ public final class MonitorWatcher {
 | 
			
		||||
            if (serverMonitor == null || monitor.enqueued) continue;
 | 
			
		||||
 | 
			
		||||
            var state = getState(monitor, serverMonitor);
 | 
			
		||||
            PlatformHelper.get().sendToPlayer(new MonitorClientMessage(monitor.getBlockPos(), state), player);
 | 
			
		||||
            ServerNetworking.sendToPlayer(new MonitorClientMessage(monitor.getBlockPos(), state), player);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -66,7 +66,7 @@ public final class MonitorWatcher {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var state = getState(tile, monitor);
 | 
			
		||||
            PlatformHelper.get().sendToAllTracking(new MonitorClientMessage(pos, state), chunk);
 | 
			
		||||
            ServerNetworking.sendToAllTracking(new MonitorClientMessage(pos, state), chunk);
 | 
			
		||||
 | 
			
		||||
            limit -= state.size();
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -225,7 +225,9 @@ public final class PrinterBlockEntity extends AbstractContainerBlockEntity imple
 | 
			
		||||
        var stack = PrintoutItem.createSingleFromTitleAndText(pageTitle, lines, colours);
 | 
			
		||||
        for (var slot : BOTTOM_SLOTS) {
 | 
			
		||||
            if (inventory.get(slot).isEmpty()) {
 | 
			
		||||
                setItem(slot, stack);
 | 
			
		||||
                inventory.set(slot, stack);
 | 
			
		||||
                updateBlockState();
 | 
			
		||||
                setChanged();
 | 
			
		||||
                printing = false;
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -15,14 +15,44 @@ import javax.annotation.Nullable;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The printer peripheral allows pages and books to be printed.
 | 
			
		||||
 * The printer peripheral allows printing text onto pages. These pages can then be crafted together into printed pages
 | 
			
		||||
 * or books.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * ## Recipe
 | 
			
		||||
 * Printers require ink (one of the coloured dyes) and paper in order to function. Once loaded, a new page can be
 | 
			
		||||
 * started with {@link #newPage()}. Then the printer can be used similarly to a normal terminal; {@linkplain
 | 
			
		||||
 * #write(Coerced) text can be written}, and {@linkplain #setCursorPos(int, int) the cursor moved}. Once all text has
 | 
			
		||||
 * been printed, {@link #endPage()} should be called to finally print the page.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * ## Recipes
 | 
			
		||||
 * <div class="recipe-container">
 | 
			
		||||
 *     <mc-recipe recipe="computercraft:printer"></mc-recipe>
 | 
			
		||||
 *     <mc-recipe recipe="computercraft:printed_pages"></mc-recipe>
 | 
			
		||||
 *     <mc-recipe recipe="computercraft:printed_book"></mc-recipe>
 | 
			
		||||
 * </div>
 | 
			
		||||
 *
 | 
			
		||||
 * @cc.usage Print a page titled "Hello" with a small message on it.
 | 
			
		||||
 *
 | 
			
		||||
 * <pre>{@code
 | 
			
		||||
 * local printer = peripheral.find("printer")
 | 
			
		||||
 *
 | 
			
		||||
 * -- Start a new page, or print an error.
 | 
			
		||||
 * if not printer.newPage() then
 | 
			
		||||
 *   error("Cannot start a new page. Do you have ink and paper?")
 | 
			
		||||
 * end
 | 
			
		||||
 *
 | 
			
		||||
 * -- Write to the page
 | 
			
		||||
 * printer.setPageTitle("Hello")
 | 
			
		||||
 * printer.write("This is my first page")
 | 
			
		||||
 * printer.setCursorPos(1, 3)
 | 
			
		||||
 * printer.write("This is two lines below.")
 | 
			
		||||
 *
 | 
			
		||||
 * -- And finally print the page!
 | 
			
		||||
 * if not printer.endPage() then
 | 
			
		||||
 *   error("Cannot end the page. Is there enough space?")
 | 
			
		||||
 * end
 | 
			
		||||
 * }</pre>
 | 
			
		||||
 * @cc.module printer
 | 
			
		||||
 * @cc.see cc.strings.wrap To wrap text before printing it.
 | 
			
		||||
 */
 | 
			
		||||
public class PrinterPeripheral implements IPeripheral {
 | 
			
		||||
    private final PrinterBlockEntity printer;
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ package dan200.computercraft.shared.peripheral.speaker;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import dan200.computercraft.core.util.Nullability;
 | 
			
		||||
import dan200.computercraft.shared.network.client.SpeakerStopClientMessage;
 | 
			
		||||
import dan200.computercraft.shared.platform.PlatformHelper;
 | 
			
		||||
import dan200.computercraft.shared.network.server.ServerNetworking;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntity;
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntityType;
 | 
			
		||||
@@ -32,7 +32,7 @@ public class SpeakerBlockEntity extends BlockEntity {
 | 
			
		||||
    public void setRemoved() {
 | 
			
		||||
        super.setRemoved();
 | 
			
		||||
        if (level != null && !level.isClientSide) {
 | 
			
		||||
            PlatformHelper.get().sendToAllPlayers(new SpeakerStopClientMessage(peripheral.getSource()), Nullability.assertNonNull(getLevel().getServer()));
 | 
			
		||||
            ServerNetworking.sendToAllPlayers(new SpeakerStopClientMessage(peripheral.getSource()), Nullability.assertNonNull(getLevel().getServer()));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ import dan200.computercraft.shared.network.client.SpeakerAudioClientMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.client.SpeakerMoveClientMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.client.SpeakerPlayClientMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.client.SpeakerStopClientMessage;
 | 
			
		||||
import dan200.computercraft.shared.platform.PlatformHelper;
 | 
			
		||||
import dan200.computercraft.shared.network.server.ServerNetworking;
 | 
			
		||||
import dan200.computercraft.shared.util.PauseAwareTimer;
 | 
			
		||||
import net.minecraft.ResourceLocationException;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
@@ -116,14 +116,14 @@ public abstract class SpeakerPeripheral implements IPeripheral {
 | 
			
		||||
        // Stop the speaker and nuke the position, so we don't update it again.
 | 
			
		||||
        if (shouldStop && lastPosition != null) {
 | 
			
		||||
            lastPosition = null;
 | 
			
		||||
            PlatformHelper.get().sendToAllPlayers(new SpeakerStopClientMessage(getSource()), server);
 | 
			
		||||
            ServerNetworking.sendToAllPlayers(new SpeakerStopClientMessage(getSource()), server);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var now = PauseAwareTimer.getTime();
 | 
			
		||||
        if (sound != null) {
 | 
			
		||||
            lastPlayTime = clock;
 | 
			
		||||
            PlatformHelper.get().sendToAllAround(
 | 
			
		||||
            ServerNetworking.sendToAllAround(
 | 
			
		||||
                new SpeakerPlayClientMessage(getSource(), position, sound.sound, sound.volume, sound.pitch),
 | 
			
		||||
                (ServerLevel) level, pos, sound.volume * 16
 | 
			
		||||
            );
 | 
			
		||||
@@ -131,7 +131,7 @@ public abstract class SpeakerPeripheral implements IPeripheral {
 | 
			
		||||
        } else if (dfpwmState != null && dfpwmState.shouldSendPending(now)) {
 | 
			
		||||
            // If clients need to receive another batch of audio, send it and then notify computers our internal buffer is
 | 
			
		||||
            // free again.
 | 
			
		||||
            PlatformHelper.get().sendToAllTracking(
 | 
			
		||||
            ServerNetworking.sendToAllTracking(
 | 
			
		||||
                new SpeakerAudioClientMessage(getSource(), position, dfpwmState.getVolume(), dfpwmState.pullPending(now)),
 | 
			
		||||
                level.getChunkAt(BlockPos.containing(pos))
 | 
			
		||||
            );
 | 
			
		||||
@@ -150,7 +150,7 @@ public abstract class SpeakerPeripheral implements IPeripheral {
 | 
			
		||||
        // in the last second.
 | 
			
		||||
        if (lastPosition != null && (clock - lastPositionTime) >= 20 && !lastPosition.withinDistance(position, 0.1)) {
 | 
			
		||||
            // TODO: What to do when entities move away? How do we notify people left behind that they're gone.
 | 
			
		||||
            PlatformHelper.get().sendToAllTracking(
 | 
			
		||||
            ServerNetworking.sendToAllTracking(
 | 
			
		||||
                new SpeakerMoveClientMessage(getSource(), position),
 | 
			
		||||
                level.getChunkAt(BlockPos.containing(pos))
 | 
			
		||||
            );
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ package dan200.computercraft.shared.peripheral.speaker;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.peripheral.IComputerAccess;
 | 
			
		||||
import dan200.computercraft.shared.network.client.SpeakerStopClientMessage;
 | 
			
		||||
import dan200.computercraft.shared.platform.PlatformHelper;
 | 
			
		||||
import dan200.computercraft.shared.network.server.ServerNetworking;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -25,6 +25,6 @@ public abstract class UpgradeSpeakerPeripheral extends SpeakerPeripheral {
 | 
			
		||||
        var server = level.getServer();
 | 
			
		||||
        if (server == null || server.isStopped()) return;
 | 
			
		||||
 | 
			
		||||
        PlatformHelper.get().sendToAllPlayers(new SpeakerStopClientMessage(getSource()), server);
 | 
			
		||||
        ServerNetworking.sendToAllPlayers(new SpeakerStopClientMessage(getSource()), server);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,9 +4,7 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.platform;
 | 
			
		||||
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.core.Direction;
 | 
			
		||||
import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
@@ -18,15 +16,11 @@ import javax.annotation.Nullable;
 | 
			
		||||
public interface ComponentAccess<T> {
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a peripheral for the current block.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * Both {@code level} and {@code pos} must be constant for the lifetime of the store.
 | 
			
		||||
     *
 | 
			
		||||
     * @param level     The current level.
 | 
			
		||||
     * @param pos       The position of the block fetching the peripheral, for instance the computer or modem.
 | 
			
		||||
     * @param direction The direction the peripheral is in.
 | 
			
		||||
     * @return The peripheral, or {@literal null} if not found.
 | 
			
		||||
     * @throws IllegalStateException If the level or position have changed.
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    T get(ServerLevel level, BlockPos pos, Direction direction);
 | 
			
		||||
    T get(Direction direction);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -20,9 +20,10 @@ import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.core.Direction;
 | 
			
		||||
import net.minecraft.core.Registry;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.network.protocol.Packet;
 | 
			
		||||
import net.minecraft.network.protocol.game.ClientGamePacketListener;
 | 
			
		||||
import net.minecraft.resources.ResourceKey;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.server.MinecraftServer;
 | 
			
		||||
import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
import net.minecraft.server.level.ServerPlayer;
 | 
			
		||||
import net.minecraft.server.level.ServerPlayerGameMode;
 | 
			
		||||
@@ -44,12 +45,10 @@ import net.minecraft.world.level.block.Block;
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntity;
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntityType;
 | 
			
		||||
import net.minecraft.world.level.block.state.BlockState;
 | 
			
		||||
import net.minecraft.world.level.chunk.LevelChunk;
 | 
			
		||||
import net.minecraft.world.phys.BlockHitResult;
 | 
			
		||||
import net.minecraft.world.phys.Vec3;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.function.BiFunction;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
@@ -178,64 +177,32 @@ public interface PlatformHelper extends dan200.computercraft.impl.PlatformHelper
 | 
			
		||||
    <T extends NetworkMessage<?>> MessageType<T> createMessageType(int id, ResourceLocation channel, Class<T> klass, FriendlyByteBuf.Reader<T> reader);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send a message to a specific player.
 | 
			
		||||
     * Convert a clientbound {@link NetworkMessage} to a Minecraft {@link Packet}.
 | 
			
		||||
     *
 | 
			
		||||
     * @param message The message to send.
 | 
			
		||||
     * @param player  The player to send it to.
 | 
			
		||||
     * @param message The messsge to convert.
 | 
			
		||||
     * @return The converted message.
 | 
			
		||||
     */
 | 
			
		||||
    void sendToPlayer(NetworkMessage<ClientNetworkContext> message, ServerPlayer player);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send a message to a set of players.
 | 
			
		||||
     *
 | 
			
		||||
     * @param message The message to send.
 | 
			
		||||
     * @param players The players to send it to.
 | 
			
		||||
     */
 | 
			
		||||
    void sendToPlayers(NetworkMessage<ClientNetworkContext> message, Collection<ServerPlayer> players);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send a message to all players.
 | 
			
		||||
     *
 | 
			
		||||
     * @param message The message to send.
 | 
			
		||||
     * @param server  The current server.
 | 
			
		||||
     */
 | 
			
		||||
    void sendToAllPlayers(NetworkMessage<ClientNetworkContext> message, MinecraftServer server);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send a message to all players around a point.
 | 
			
		||||
     *
 | 
			
		||||
     * @param message  The message to send.
 | 
			
		||||
     * @param level    The level the point is in.
 | 
			
		||||
     * @param pos      The centre position.
 | 
			
		||||
     * @param distance The distance to the centre players must be within.
 | 
			
		||||
     */
 | 
			
		||||
    void sendToAllAround(NetworkMessage<ClientNetworkContext> message, ServerLevel level, Vec3 pos, float distance);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send a message to all players tracking a chunk.
 | 
			
		||||
     *
 | 
			
		||||
     * @param message The message to send.
 | 
			
		||||
     * @param chunk   The chunk players must be tracking.
 | 
			
		||||
     */
 | 
			
		||||
    void sendToAllTracking(NetworkMessage<ClientNetworkContext> message, LevelChunk chunk);
 | 
			
		||||
    Packet<ClientGamePacketListener> createPacket(NetworkMessage<ClientNetworkContext> message);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a {@link ComponentAccess} for surrounding peripherals.
 | 
			
		||||
     *
 | 
			
		||||
     * @param owner      The block entity requesting surrounding peripherals.
 | 
			
		||||
     * @param invalidate The function to call when a neighbouring peripheral potentially changes. This <em>MAY NOT</em>
 | 
			
		||||
     *                   include all changes, and so block updates should still be listened to.
 | 
			
		||||
     * @return The peripheral component access.
 | 
			
		||||
     */
 | 
			
		||||
    ComponentAccess<IPeripheral> createPeripheralAccess(Consumer<Direction> invalidate);
 | 
			
		||||
    ComponentAccess<IPeripheral> createPeripheralAccess(BlockEntity owner, Consumer<Direction> invalidate);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a {@link ComponentAccess} for surrounding wired nodes.
 | 
			
		||||
     *
 | 
			
		||||
     * @param owner      The block entity requesting surrounding wired elements.
 | 
			
		||||
     * @param invalidate The function to call when a neighbouring wired node potentially changes. This <em>MAY NOT</em>
 | 
			
		||||
     *                   include all changes, and so block updates should still be listened to.
 | 
			
		||||
     * @return The peripheral component access.
 | 
			
		||||
     */
 | 
			
		||||
    ComponentAccess<WiredElement> createWiredElementAccess(Consumer<Direction> invalidate);
 | 
			
		||||
    ComponentAccess<WiredElement> createWiredElementAccess(BlockEntity owner, Consumer<Direction> invalidate);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Determine if there is a wired element in the given direction. This is equivalent to
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ import dan200.computercraft.shared.computer.core.ServerComputer;
 | 
			
		||||
import dan200.computercraft.shared.config.Config;
 | 
			
		||||
import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.client.PocketComputerDeletedClientMessage;
 | 
			
		||||
import dan200.computercraft.shared.platform.PlatformHelper;
 | 
			
		||||
import dan200.computercraft.shared.network.server.ServerNetworking;
 | 
			
		||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.nbt.CompoundTag;
 | 
			
		||||
@@ -161,7 +161,7 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
 | 
			
		||||
        if (sendState) {
 | 
			
		||||
            // Broadcast the state to all players
 | 
			
		||||
            tracking.addAll(getLevel().players());
 | 
			
		||||
            PlatformHelper.get().sendToPlayers(new PocketComputerDataMessage(this, false), tracking);
 | 
			
		||||
            ServerNetworking.sendToPlayers(new PocketComputerDataMessage(this, false), tracking);
 | 
			
		||||
        } else {
 | 
			
		||||
            // Broadcast the state to new players.
 | 
			
		||||
            List<ServerPlayer> added = new ArrayList<>();
 | 
			
		||||
@@ -169,7 +169,7 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
 | 
			
		||||
                if (tracking.add(player)) added.add(player);
 | 
			
		||||
            }
 | 
			
		||||
            if (!added.isEmpty()) {
 | 
			
		||||
                PlatformHelper.get().sendToPlayers(new PocketComputerDataMessage(this, false), added);
 | 
			
		||||
                ServerNetworking.sendToPlayers(new PocketComputerDataMessage(this, false), added);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -180,13 +180,13 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
 | 
			
		||||
 | 
			
		||||
        if (entity instanceof ServerPlayer player && entity.isAlive()) {
 | 
			
		||||
            // Broadcast the terminal to the current player.
 | 
			
		||||
            PlatformHelper.get().sendToPlayer(new PocketComputerDataMessage(this, true), player);
 | 
			
		||||
            ServerNetworking.sendToPlayer(new PocketComputerDataMessage(this, true), player);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onRemoved() {
 | 
			
		||||
        super.onRemoved();
 | 
			
		||||
        PlatformHelper.get().sendToAllPlayers(new PocketComputerDeletedClientMessage(getInstanceID()), getLevel().getServer());
 | 
			
		||||
        ServerNetworking.sendToAllPlayers(new PocketComputerDeletedClientMessage(getInstanceID()), getLevel().getServer());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,6 @@ import net.minecraft.world.phys.Vec3;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
import java.util.function.Predicate;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.shared.common.IColouredItem.NBT_COLOUR;
 | 
			
		||||
import static dan200.computercraft.shared.util.WaterloggableHelpers.WATERLOGGED;
 | 
			
		||||
@@ -59,8 +58,6 @@ public class TurtleBrain implements TurtleAccessInternal {
 | 
			
		||||
 | 
			
		||||
    private static final int ANIM_DURATION = 8;
 | 
			
		||||
 | 
			
		||||
    public static final Predicate<Entity> PUSHABLE_ENTITY = entity -> !entity.isSpectator() && entity.getPistonPushReaction() != PushReaction.IGNORE;
 | 
			
		||||
 | 
			
		||||
    private TurtleBlockEntity owner;
 | 
			
		||||
    private @Nullable GameProfile owningPlayer;
 | 
			
		||||
 | 
			
		||||
@@ -694,7 +691,7 @@ public class TurtleBrain implements TurtleAccessInternal {
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    var aabb = new AABB(minX, minY, minZ, maxX, maxY, maxZ);
 | 
			
		||||
                    var list = world.getEntitiesOfClass(Entity.class, aabb, PUSHABLE_ENTITY);
 | 
			
		||||
                    var list = world.getEntitiesOfClass(Entity.class, aabb, TurtleBrain::canPush);
 | 
			
		||||
                    if (!list.isEmpty()) {
 | 
			
		||||
                        double pushStep = 1.0f / ANIM_DURATION;
 | 
			
		||||
                        var pushStepX = moveDir.getStepX() * pushStep;
 | 
			
		||||
@@ -737,6 +734,10 @@ public class TurtleBrain implements TurtleAccessInternal {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static boolean canPush(Entity entity) {
 | 
			
		||||
        return !entity.isSpectator() && entity.getPistonPushReaction() != PushReaction.IGNORE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private float getAnimationFraction(float f) {
 | 
			
		||||
        var next = (float) animationProgress / ANIM_DURATION;
 | 
			
		||||
        var previous = (float) lastAnimationProgress / ANIM_DURATION;
 | 
			
		||||
 
 | 
			
		||||
@@ -19,9 +19,7 @@ import java.io.IOException;
 | 
			
		||||
import java.io.OutputStream;
 | 
			
		||||
import java.security.MessageDigest;
 | 
			
		||||
import java.security.NoSuchAlgorithmException;
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
 | 
			
		||||
public final class NBTUtil {
 | 
			
		||||
    private static final Logger LOG = LoggerFactory.getLogger(NBTUtil.class);
 | 
			
		||||
@@ -149,20 +147,20 @@ public final class NBTUtil {
 | 
			
		||||
            }
 | 
			
		||||
            case Tag.TAG_LIST: {
 | 
			
		||||
                var list = (ListTag) tag;
 | 
			
		||||
                Map<Integer, Object> map = new HashMap<>(list.size());
 | 
			
		||||
                for (var i = 0; i < list.size(); i++) map.put(i, toLua(list.get(i)));
 | 
			
		||||
                List<Object> map = new ArrayList<>(list.size());
 | 
			
		||||
                for (var value : list) map.add(toLua(value));
 | 
			
		||||
                return map;
 | 
			
		||||
            }
 | 
			
		||||
            case Tag.TAG_BYTE_ARRAY: {
 | 
			
		||||
                var array = ((ByteArrayTag) tag).getAsByteArray();
 | 
			
		||||
                Map<Integer, Byte> map = new HashMap<>(array.length);
 | 
			
		||||
                for (var i = 0; i < array.length; i++) map.put(i + 1, array[i]);
 | 
			
		||||
                List<Byte> map = new ArrayList<>(array.length);
 | 
			
		||||
                for (var b : array) map.add(b);
 | 
			
		||||
                return map;
 | 
			
		||||
            }
 | 
			
		||||
            case Tag.TAG_INT_ARRAY: {
 | 
			
		||||
                var array = ((IntArrayTag) tag).getAsIntArray();
 | 
			
		||||
                Map<Integer, Integer> map = new HashMap<>(array.length);
 | 
			
		||||
                for (var i = 0; i < array.length; i++) map.put(i + 1, array[i]);
 | 
			
		||||
                List<Integer> map = new ArrayList<>(array.length);
 | 
			
		||||
                for (var j : array) map.add(j);
 | 
			
		||||
                return map;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -27,12 +27,8 @@ import net.minecraft.world.phys.shapes.CollisionContext;
 | 
			
		||||
import net.minecraft.world.phys.shapes.VoxelShape;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.function.Predicate;
 | 
			
		||||
 | 
			
		||||
public final class WorldUtil {
 | 
			
		||||
    @SuppressWarnings("UnnecessaryLambda")
 | 
			
		||||
    private static final Predicate<Entity> CAN_COLLIDE = x -> x != null && x.isAlive() && x.isPickable();
 | 
			
		||||
 | 
			
		||||
    public static boolean isLiquidBlock(Level world, BlockPos pos) {
 | 
			
		||||
        if (!world.isInWorldBounds(pos)) return false;
 | 
			
		||||
        return world.getBlockState(pos).liquid();
 | 
			
		||||
@@ -84,7 +80,7 @@ public final class WorldUtil {
 | 
			
		||||
        Entity bestEntity = null;
 | 
			
		||||
        Vec3 bestHit = null;
 | 
			
		||||
 | 
			
		||||
        for (var entity : level.getEntities(source, bounds, WorldUtil.CAN_COLLIDE)) {
 | 
			
		||||
        for (var entity : level.getEntities(source, bounds, WorldUtil::canCollide)) {
 | 
			
		||||
            var aabb = entity.getBoundingBox().inflate(entity.getPickRadius());
 | 
			
		||||
 | 
			
		||||
            // clip doesn't work when inside the entity. Just assume we've got a perfect match and break.
 | 
			
		||||
@@ -109,6 +105,10 @@ public final class WorldUtil {
 | 
			
		||||
        return bestEntity == null ? null : new EntityHitResult(bestEntity, bestHit);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static boolean canCollide(Entity entity) {
 | 
			
		||||
        return entity != null && entity.isAlive() && entity.isPickable();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static Vec3 getRayStart(Player entity) {
 | 
			
		||||
        return entity.getEyePosition();
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
    "required": true,
 | 
			
		||||
    "package": "dan200.computercraft.mixin",
 | 
			
		||||
    "minVersion": "0.8",
 | 
			
		||||
    "compatibilityLevel": "JAVA_17",
 | 
			
		||||
    "injectors": {
 | 
			
		||||
        "defaultRequire": 1
 | 
			
		||||
    },
 | 
			
		||||
    "mixins": [
 | 
			
		||||
        "CacheUpdaterMixin"
 | 
			
		||||
    ],
 | 
			
		||||
    "refmap": "computercraft.refmap.json"
 | 
			
		||||
}
 | 
			
		||||
@@ -18,15 +18,18 @@ import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.client.ClientNetworkContext;
 | 
			
		||||
import dan200.computercraft.shared.network.container.ContainerData;
 | 
			
		||||
import dan200.computercraft.shared.platform.*;
 | 
			
		||||
import io.netty.buffer.Unpooled;
 | 
			
		||||
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.core.Direction;
 | 
			
		||||
import net.minecraft.core.Registry;
 | 
			
		||||
import net.minecraft.core.registries.BuiltInRegistries;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.network.protocol.Packet;
 | 
			
		||||
import net.minecraft.network.protocol.game.ClientGamePacketListener;
 | 
			
		||||
import net.minecraft.network.protocol.game.ClientboundCustomPayloadPacket;
 | 
			
		||||
import net.minecraft.resources.ResourceKey;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.server.MinecraftServer;
 | 
			
		||||
import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
import net.minecraft.server.level.ServerPlayer;
 | 
			
		||||
import net.minecraft.tags.TagKey;
 | 
			
		||||
@@ -47,12 +50,10 @@ import net.minecraft.world.level.block.Block;
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntity;
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntityType;
 | 
			
		||||
import net.minecraft.world.level.block.state.BlockState;
 | 
			
		||||
import net.minecraft.world.level.chunk.LevelChunk;
 | 
			
		||||
import net.minecraft.world.phys.BlockHitResult;
 | 
			
		||||
import net.minecraft.world.phys.Vec3;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.Iterator;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.function.BiFunction;
 | 
			
		||||
@@ -129,31 +130,6 @@ public class TestPlatformHelper extends AbstractComputerCraftAPI implements Plat
 | 
			
		||||
        throw new UnsupportedOperationException("Cannot register ArgumentTypeInfo inside tests");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void sendToPlayer(NetworkMessage<ClientNetworkContext> message, ServerPlayer player) {
 | 
			
		||||
        throw new UnsupportedOperationException("Cannot send NetworkMessages inside tests");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void sendToPlayers(NetworkMessage<ClientNetworkContext> message, Collection<ServerPlayer> players) {
 | 
			
		||||
        throw new UnsupportedOperationException("Cannot send NetworkMessages inside tests");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void sendToAllPlayers(NetworkMessage<ClientNetworkContext> message, MinecraftServer server) {
 | 
			
		||||
        throw new UnsupportedOperationException("Cannot send NetworkMessages inside tests");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void sendToAllAround(NetworkMessage<ClientNetworkContext> message, ServerLevel level, Vec3 pos, float distance) {
 | 
			
		||||
        throw new UnsupportedOperationException("Cannot send NetworkMessages inside tests");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void sendToAllTracking(NetworkMessage<ClientNetworkContext> message, LevelChunk chunk) {
 | 
			
		||||
        throw new UnsupportedOperationException("Cannot send NetworkMessages inside tests");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public List<TagKey<Item>> getDyeTags() {
 | 
			
		||||
        throw new UnsupportedOperationException("Cannot query tags inside tests");
 | 
			
		||||
@@ -169,20 +145,30 @@ public class TestPlatformHelper extends AbstractComputerCraftAPI implements Plat
 | 
			
		||||
        throw new UnsupportedOperationException("Cannot open menu inside tests");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public <T extends NetworkMessage<?>> MessageType<T> createMessageType(int id, ResourceLocation channel, Class<T> klass, FriendlyByteBuf.Reader<T> reader) {
 | 
			
		||||
        record TypeImpl<T extends NetworkMessage<?>>(Function<FriendlyByteBuf, T> reader) implements MessageType<T> {
 | 
			
		||||
        }
 | 
			
		||||
        return new TypeImpl<>(reader);
 | 
			
		||||
    record TypeImpl<T extends NetworkMessage<?>>(
 | 
			
		||||
        ResourceLocation id, Function<FriendlyByteBuf, T> reader
 | 
			
		||||
    ) implements MessageType<T> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public ComponentAccess<IPeripheral> createPeripheralAccess(Consumer<Direction> invalidate) {
 | 
			
		||||
    public <T extends NetworkMessage<?>> MessageType<T> createMessageType(int id, ResourceLocation channel, Class<T> klass, FriendlyByteBuf.Reader<T> reader) {
 | 
			
		||||
        return new TypeImpl<>(channel, reader);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Packet<ClientGamePacketListener> createPacket(NetworkMessage<ClientNetworkContext> message) {
 | 
			
		||||
        var buf = new FriendlyByteBuf(Unpooled.buffer());
 | 
			
		||||
        message.write(buf);
 | 
			
		||||
        return new ClientboundCustomPayloadPacket(((TypeImpl<?>) message.type()).id(), buf);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public ComponentAccess<IPeripheral> createPeripheralAccess(BlockEntity owner, Consumer<Direction> invalidate) {
 | 
			
		||||
        throw new UnsupportedOperationException("Cannot interact with the world inside tests");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public ComponentAccess<WiredElement> createWiredElementAccess(Consumer<Direction> invalidate) {
 | 
			
		||||
    public ComponentAccess<WiredElement> createWiredElementAccess(BlockEntity owner, Consumer<Direction> invalidate) {
 | 
			
		||||
        throw new UnsupportedOperationException("Cannot interact with the world inside tests");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,9 +4,10 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.sound;
 | 
			
		||||
 | 
			
		||||
import io.netty.buffer.ByteBufAllocator;
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertEquals;
 | 
			
		||||
 | 
			
		||||
public class DfpwmStreamTest {
 | 
			
		||||
@@ -14,8 +15,7 @@ public class DfpwmStreamTest {
 | 
			
		||||
    public void testDecodesBytes() {
 | 
			
		||||
        var stream = new DfpwmStream();
 | 
			
		||||
 | 
			
		||||
        var input = ByteBufAllocator.DEFAULT.buffer();
 | 
			
		||||
        input.writeBytes(new byte[]{ 43, -31, 33, 44, 30, -16, -85, 23, -3, -55, 46, -70, 68, -67, 74, -96, -68, 16, 94, -87, -5, 87, 11, -16, 19, 92, 85, -71, 126, 5, -84, 64, 17, -6, 85, -11, -1, -87, -12, 1, 85, -56, 33, -80, 82, 104, -93, 17, 126, 23, 91, -30, 37, -32, 117, -72, -58, 11, -76, 19, -108, 86, -65, -10, -1, -68, -25, 10, -46, 85, 124, -54, 15, -24, 43, -94, 117, 63, -36, 15, -6, 88, 87, -26, -83, 106, 41, 13, -28, -113, -10, -66, 119, -87, -113, 68, -55, 40, -107, 62, 20, 72, 3, -96, 114, -87, -2, 39, -104, 30, 20, 42, 84, 24, 47, 64, 43, 61, -35, 95, -65, 42, 61, 42, -50, 4, -9, 81 });
 | 
			
		||||
        var input = ByteBuffer.wrap(new byte[]{ 43, -31, 33, 44, 30, -16, -85, 23, -3, -55, 46, -70, 68, -67, 74, -96, -68, 16, 94, -87, -5, 87, 11, -16, 19, 92, 85, -71, 126, 5, -84, 64, 17, -6, 85, -11, -1, -87, -12, 1, 85, -56, 33, -80, 82, 104, -93, 17, 126, 23, 91, -30, 37, -32, 117, -72, -58, 11, -76, 19, -108, 86, -65, -10, -1, -68, -25, 10, -46, 85, 124, -54, 15, -24, 43, -94, 117, 63, -36, 15, -6, 88, 87, -26, -83, 106, 41, 13, -28, -113, -10, -66, 119, -87, -113, 68, -55, 40, -107, 62, 20, 72, 3, -96, 114, -87, -2, 39, -104, 30, 20, 42, 84, 24, 47, 64, 43, 61, -35, 95, -65, 42, 61, 42, -50, 4, -9, 81 });
 | 
			
		||||
        stream.push(input);
 | 
			
		||||
 | 
			
		||||
        var buffer = stream.read(1024 + 1);
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
package dan200.computercraft.gametest
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.core.apis.FSAPI
 | 
			
		||||
import dan200.computercraft.core.util.Colour
 | 
			
		||||
import dan200.computercraft.gametest.api.*
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry
 | 
			
		||||
import dan200.computercraft.shared.media.items.DiskItem
 | 
			
		||||
@@ -19,6 +20,9 @@ import net.minecraft.network.chat.Component
 | 
			
		||||
import net.minecraft.world.item.ItemStack
 | 
			
		||||
import net.minecraft.world.item.Items
 | 
			
		||||
import net.minecraft.world.level.block.RedStoneWireBlock
 | 
			
		||||
import org.hamcrest.MatcherAssert.assertThat
 | 
			
		||||
import org.hamcrest.Matchers.array
 | 
			
		||||
import org.hamcrest.Matchers.equalTo
 | 
			
		||||
import org.junit.jupiter.api.Assertions.assertEquals
 | 
			
		||||
 | 
			
		||||
class Disk_Drive_Test {
 | 
			
		||||
@@ -45,14 +49,48 @@ class Disk_Drive_Test {
 | 
			
		||||
        thenWaitUntil { helper.assertItemEntityPresent(Items.MUSIC_DISC_13, stackAt, 0.0) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A mount is initially attached, and then removed when the disk is ejected.
 | 
			
		||||
     */
 | 
			
		||||
    @GameTest
 | 
			
		||||
    fun Queues_event(helper: GameTestHelper) = helper.sequence {
 | 
			
		||||
        val pos = BlockPos(1, 2, 2)
 | 
			
		||||
 | 
			
		||||
        var started = false
 | 
			
		||||
        var disk = false
 | 
			
		||||
        var ejected = false
 | 
			
		||||
        thenStartComputer {
 | 
			
		||||
            // thenOnComputer discards events, so instead we need to track our state transitions.
 | 
			
		||||
            started = true
 | 
			
		||||
 | 
			
		||||
            val diskEvent = pullEvent("disk")
 | 
			
		||||
            assertThat(diskEvent, array(equalTo("disk"), equalTo("right")))
 | 
			
		||||
 | 
			
		||||
            disk = true
 | 
			
		||||
 | 
			
		||||
            val ejectEvent = pullEvent("disk_eject")
 | 
			
		||||
            assertThat(ejectEvent, array(equalTo("disk_eject"), equalTo("right")))
 | 
			
		||||
 | 
			
		||||
            ejected = true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        thenWaitUntil { helper.assertTrue(started, "Computer not started") }
 | 
			
		||||
        thenExecute { helper.setContainerItem(pos, 0, ItemStack(Items.DIRT)) }
 | 
			
		||||
        thenWaitUntil { helper.assertTrue(disk, "disk not inserted") }
 | 
			
		||||
        thenExecute { helper.setContainerItem(pos, 0, ItemStack.EMPTY) }
 | 
			
		||||
        thenWaitUntil { helper.assertTrue(ejected, "disk not ejected") }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A mount is initially attached, and then removed when the disk is ejected.
 | 
			
		||||
     */
 | 
			
		||||
    @GameTest
 | 
			
		||||
    fun Adds_removes_mount(helper: GameTestHelper) = helper.sequence {
 | 
			
		||||
        thenOnComputer { } // Wait for the computer to start up
 | 
			
		||||
        thenIdle(2) // Let the disk drive tick once to create the mount
 | 
			
		||||
        thenOnComputer { // Then actually assert things!
 | 
			
		||||
        thenExecute {
 | 
			
		||||
            helper.setContainerItem(BlockPos(1, 2, 2), 0, DiskItem.createFromIDAndColour(1, null, Colour.BLACK.hex))
 | 
			
		||||
        }
 | 
			
		||||
        thenOnComputer {
 | 
			
		||||
            getApi<FSAPI>().getDrive("disk").assertArrayEquals("right")
 | 
			
		||||
            callPeripheral("right", "ejectDisk")
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ class Inventory_Test {
 | 
			
		||||
     *
 | 
			
		||||
     * @see <https://github.com/cc-tweaked/cc-restitched/issues/121>
 | 
			
		||||
     */
 | 
			
		||||
    @GameTest(required = false)
 | 
			
		||||
    @GameTest
 | 
			
		||||
    fun Checks_valid_item(helper: GameTestHelper) = helper.sequence {
 | 
			
		||||
        thenOnComputer {
 | 
			
		||||
            getApi<PeripheralAPI>().call(
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,10 @@ package dan200.computercraft.gametest
 | 
			
		||||
import dan200.computercraft.api.lua.ObjectArguments
 | 
			
		||||
import dan200.computercraft.core.apis.PeripheralAPI
 | 
			
		||||
import dan200.computercraft.core.computer.ComputerSide
 | 
			
		||||
import dan200.computercraft.gametest.api.*
 | 
			
		||||
import dan200.computercraft.gametest.api.getBlockEntity
 | 
			
		||||
import dan200.computercraft.gametest.api.sequence
 | 
			
		||||
import dan200.computercraft.gametest.api.thenOnComputer
 | 
			
		||||
import dan200.computercraft.gametest.api.thenStartComputer
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry
 | 
			
		||||
import dan200.computercraft.shared.peripheral.modem.wired.CableBlock
 | 
			
		||||
import dan200.computercraft.test.core.assertArrayEquals
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,6 @@ import net.minecraft.world.entity.EntityType
 | 
			
		||||
import net.minecraft.world.item.ItemStack
 | 
			
		||||
import net.minecraft.world.level.block.Blocks
 | 
			
		||||
import org.junit.jupiter.api.Assertions.*
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
class Monitor_Test {
 | 
			
		||||
    @GameTest
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,6 @@ import net.minecraft.world.inventory.MenuType
 | 
			
		||||
import net.minecraft.world.inventory.TransientCraftingContainer
 | 
			
		||||
import net.minecraft.world.item.ItemStack
 | 
			
		||||
import net.minecraft.world.item.Items
 | 
			
		||||
import net.minecraft.world.item.crafting.CraftingRecipe
 | 
			
		||||
import net.minecraft.world.item.crafting.RecipeType
 | 
			
		||||
import org.junit.jupiter.api.Assertions.assertEquals
 | 
			
		||||
import java.util.*
 | 
			
		||||
@@ -37,11 +36,11 @@ class Recipe_Test {
 | 
			
		||||
            container.setItem(0, ItemStack(Items.SKELETON_SKULL))
 | 
			
		||||
            container.setItem(1, ItemStack(ModRegistry.Items.COMPUTER_ADVANCED.get()))
 | 
			
		||||
 | 
			
		||||
            val recipe: Optional<CraftingRecipe> = context.level.server.recipeManager
 | 
			
		||||
            val recipe = context.level.server.recipeManager
 | 
			
		||||
                .getRecipeFor(RecipeType.CRAFTING, container, context.level)
 | 
			
		||||
            if (!recipe.isPresent) throw GameTestAssertException("No recipe matches")
 | 
			
		||||
                .orElseThrow { GameTestAssertException("No recipe matches") }
 | 
			
		||||
 | 
			
		||||
            val result = recipe.get().assemble(container, context.level.registryAccess())
 | 
			
		||||
            val result = recipe.assemble(container, context.level.registryAccess())
 | 
			
		||||
 | 
			
		||||
            val profile = GameProfile(UUID.fromString("f3c8d69b-0776-4512-8434-d1b2165909eb"), "dan200")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.gametest.api
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.peripheral.IPeripheral
 | 
			
		||||
import dan200.computercraft.gametest.core.ManagedComputers
 | 
			
		||||
import dan200.computercraft.mixin.gametest.GameTestHelperAccessor
 | 
			
		||||
import dan200.computercraft.mixin.gametest.GameTestInfoAccessor
 | 
			
		||||
@@ -21,6 +22,8 @@ import net.minecraft.world.Container
 | 
			
		||||
import net.minecraft.world.entity.Entity
 | 
			
		||||
import net.minecraft.world.entity.EntityType
 | 
			
		||||
import net.minecraft.world.item.ItemStack
 | 
			
		||||
import net.minecraft.world.level.block.Blocks
 | 
			
		||||
import net.minecraft.world.level.block.entity.BarrelBlockEntity
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntity
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntityType
 | 
			
		||||
import net.minecraft.world.level.block.state.BlockState
 | 
			
		||||
@@ -165,6 +168,16 @@ fun <T : Comparable<T>> GameTestHelper.assertBlockHas(pos: BlockPos, property: P
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get a [Container] at a given position.
 | 
			
		||||
 */
 | 
			
		||||
fun GameTestHelper.getContainerAt(pos: BlockPos): Container =
 | 
			
		||||
    when (val container = getBlockEntity(pos)) {
 | 
			
		||||
        is Container -> container
 | 
			
		||||
        null -> failVerbose("Expected a container at $pos, found nothing", pos)
 | 
			
		||||
        else -> failVerbose("Expected a container at $pos, found ${getName(container.type)}", pos)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Assert a container contains exactly these items and no more.
 | 
			
		||||
 *
 | 
			
		||||
@@ -173,10 +186,7 @@ fun <T : Comparable<T>> GameTestHelper.assertBlockHas(pos: BlockPos, property: P
 | 
			
		||||
 * first `n` slots - the remaining are required to be empty.
 | 
			
		||||
 */
 | 
			
		||||
fun GameTestHelper.assertContainerExactly(pos: BlockPos, items: List<ItemStack>) =
 | 
			
		||||
    when (val container = getBlockEntity(pos) ?: failVerbose("Expected a container at $pos, found nothing", pos)) {
 | 
			
		||||
        is Container -> assertContainerExactlyImpl(pos, container, items)
 | 
			
		||||
        else -> failVerbose("Expected a container at $pos, found ${getName(container.type)}", pos)
 | 
			
		||||
    }
 | 
			
		||||
    assertContainerExactlyImpl(pos, getContainerAt(pos), items)
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Assert an container contains exactly these items and no more.
 | 
			
		||||
@@ -206,9 +216,17 @@ private fun GameTestHelper.assertContainerExactlyImpl(pos: BlockPos, container:
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A nasty hack to get a peripheral at a given position, by creating a dummy [BlockEntity].
 | 
			
		||||
 */
 | 
			
		||||
private fun GameTestHelper.getPeripheralAt(pos: BlockPos, direction: Direction): IPeripheral? {
 | 
			
		||||
    val be = BarrelBlockEntity(absolutePos(pos).relative(direction), Blocks.BARREL.defaultBlockState())
 | 
			
		||||
    be.setLevel(level)
 | 
			
		||||
    return PlatformHelper.get().createPeripheralAccess(be) { }.get(direction.opposite)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun GameTestHelper.assertPeripheral(pos: BlockPos, direction: Direction = Direction.UP, type: String) {
 | 
			
		||||
    val peripheral = PlatformHelper.get().createPeripheralAccess { }
 | 
			
		||||
        .get(level, absolutePos(pos).relative(direction), direction.opposite)
 | 
			
		||||
    val peripheral = getPeripheralAt(pos, direction)
 | 
			
		||||
    when {
 | 
			
		||||
        peripheral == null -> fail("No peripheral at position", pos)
 | 
			
		||||
        peripheral.type != type -> fail("Peripheral is of type ${peripheral.type}, expected $type", pos)
 | 
			
		||||
@@ -216,8 +234,7 @@ fun GameTestHelper.assertPeripheral(pos: BlockPos, direction: Direction = Direct
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun GameTestHelper.assertNoPeripheral(pos: BlockPos, direction: Direction = Direction.UP) {
 | 
			
		||||
    val peripheral = PlatformHelper.get().createPeripheralAccess { }
 | 
			
		||||
        .get(level, absolutePos(pos).relative(direction), direction.opposite)
 | 
			
		||||
    val peripheral = getPeripheralAt(pos, direction)
 | 
			
		||||
    if (peripheral != null) fail("Expected no peripheral, got a ${peripheral.type}", pos)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -277,3 +294,13 @@ fun GameTestHelper.setBlock(pos: BlockPos, state: BlockInput) = state.place(leve
 | 
			
		||||
fun GameTestHelper.modifyBlock(pos: BlockPos, modify: (BlockState) -> BlockState) {
 | 
			
		||||
    setBlock(pos, modify(getBlockState(pos)))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Update items in the container at [pos], setting the item in the specified [slot] to [item], and then marking it
 | 
			
		||||
 * changed.
 | 
			
		||||
 */
 | 
			
		||||
fun GameTestHelper.setContainerItem(pos: BlockPos, slot: Int, item: ItemStack) {
 | 
			
		||||
    val container = getContainerAt(pos)
 | 
			
		||||
    container.setItem(slot, item)
 | 
			
		||||
    container.setChanged()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@
 | 
			
		||||
        {pos: [0, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 2], state: "computercraft:disk_drive{facing:north,state:full}", nbt: {Item: {Count: 1b, id: "computercraft:disk", tag: {Color: 1118481, DiskId: 0}}, id: "computercraft:disk_drive"}},
 | 
			
		||||
        {pos: [1, 1, 2], state: "computercraft:disk_drive{facing:north,state:full}", nbt: {id: "computercraft:disk_drive"}},
 | 
			
		||||
        {pos: [1, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 0], state: "minecraft:air"},
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										138
									
								
								projects/common/src/testMod/resources/data/cctest/structures/disk_drive_test.queues_event.snbt
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								projects/common/src/testMod/resources/data/cctest/structures/disk_drive_test.queues_event.snbt
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,138 @@
 | 
			
		||||
{
 | 
			
		||||
    DataVersion: 2975,
 | 
			
		||||
    size: [5, 5, 5],
 | 
			
		||||
    data: [
 | 
			
		||||
        {pos: [0, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 2], state: "computercraft:disk_drive{facing:north,state:full}", nbt: {id: "computercraft:disk_drive"}},
 | 
			
		||||
        {pos: [1, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 2], state: "computercraft:computer_advanced{facing:north,state:blinking}", nbt: {ComputerId: 1, Label: "disk_drive_test.queues_event", On: 1b, id: "computercraft:computer_advanced"}},
 | 
			
		||||
        {pos: [2, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 4], state: "minecraft:air"}
 | 
			
		||||
    ],
 | 
			
		||||
    entities: [],
 | 
			
		||||
    palette: [
 | 
			
		||||
        "minecraft:polished_andesite",
 | 
			
		||||
        "minecraft:air",
 | 
			
		||||
        "computercraft:disk_drive{facing:north,state:full}",
 | 
			
		||||
        "computercraft:computer_advanced{facing:north,state:blinking}"
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
@@ -4,6 +4,8 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.lua;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Represents a Lua object which is stored as a global variable on computer startup. This must either provide
 | 
			
		||||
 * {@link LuaFunction} annotated functions or implement {@link IDynamicLuaObject}.
 | 
			
		||||
@@ -15,12 +17,31 @@ package dan200.computercraft.api.lua;
 | 
			
		||||
 */
 | 
			
		||||
public interface ILuaAPI {
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the globals this API will be assigned to. This will override any other global, so you should
 | 
			
		||||
     * Get the globals this API will be assigned to.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * This will override any other global, so you should be careful to pick a unique name. Alternatively, you may
 | 
			
		||||
     * return the empty array here, and instead override {@link #getModuleName()}.
 | 
			
		||||
     *
 | 
			
		||||
     * @return A list of globals this API will be assigned to.
 | 
			
		||||
     */
 | 
			
		||||
    String[] getNames();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the module name this API should be available as.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * Rather than (or as well as) making this API available as a global, APIs can be exposed as {@code require}able
 | 
			
		||||
     * modules. This is generally more idiomatic, as it avoids polluting the global environment.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * Modules defined here take precedence over user-defined modules, and so like with {@link #getNames()}, you should
 | 
			
		||||
     * be careful to pick a unique name. It is recommended that module names should be camel case, and live under a
 | 
			
		||||
     * namespace associated with your mod. For instance, {@code "mod_id.a_custom_api"}.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The module name of this API, or {@code null} if this API should not be loadable as a module.
 | 
			
		||||
     */
 | 
			
		||||
    default @Nullable String getModuleName() {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when the computer is turned on.
 | 
			
		||||
     * <p>
 | 
			
		||||
 
 | 
			
		||||
@@ -50,13 +50,6 @@ tasks.test {
 | 
			
		||||
    systemProperty("cct.test-files", layout.buildDirectory.dir("tmp/testFiles").getAbsolutePath())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tasks.testFixturesJar {
 | 
			
		||||
    manifest {
 | 
			
		||||
        // Ensure the test fixtures jar loads as a mod. Thanks FML >_>.
 | 
			
		||||
        attributes("FMLModType" to "GAMELIBRARY")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
val checkChangelog by tasks.registering(cc.tweaked.gradle.CheckChangelog::class) {
 | 
			
		||||
    version.set(modVersion)
 | 
			
		||||
    whatsNew.set(file("src/main/resources/data/computercraft/lua/rom/help/whatsnew.md"))
 | 
			
		||||
 
 | 
			
		||||
@@ -100,8 +100,8 @@ public abstract class AbstractHandle {
 | 
			
		||||
    /**
 | 
			
		||||
     * Read a number of bytes from this file.
 | 
			
		||||
     *
 | 
			
		||||
     * @param countArg The number of bytes to read. When absent, a single byte will be read <em>as a number</em>. This
 | 
			
		||||
     *                 may be 0 to determine we are at the end of the file.
 | 
			
		||||
     * @param countArg The number of bytes to read. This may be 0 to determine we are at the end of the file. When
 | 
			
		||||
     *                 absent, a single byte will be read.
 | 
			
		||||
     * @return The read bytes.
 | 
			
		||||
     * @throws LuaException When trying to read a negative number of bytes.
 | 
			
		||||
     * @throws LuaException If the file has been closed.
 | 
			
		||||
 
 | 
			
		||||
@@ -835,8 +835,8 @@ public final class ComputerThread implements ComputerScheduler {
 | 
			
		||||
                var allocated = ThreadAllocations.getAllocatedBytes(current) - info.allocatedBytes();
 | 
			
		||||
                if (allocated > 0) {
 | 
			
		||||
                    metrics.observe(Metrics.JAVA_ALLOCATION, allocated);
 | 
			
		||||
                } else {
 | 
			
		||||
                    LOG.warn("Allocated a negative number of bytes!");
 | 
			
		||||
                } else if (allocated < 0) {
 | 
			
		||||
                    LOG.warn("Allocated a negative number of bytes ({})!", allocated);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -275,9 +275,9 @@ public final class MemoryMount extends AbstractInMemoryMount<MemoryMount.FileEnt
 | 
			
		||||
            checkClosed();
 | 
			
		||||
 | 
			
		||||
            var backing = Nullability.assertNonNull(entry.contents);
 | 
			
		||||
            if (position >= backing.length) return -1;
 | 
			
		||||
            if (position >= entry.length) return -1;
 | 
			
		||||
 | 
			
		||||
            var remaining = Math.min(backing.length - (int) position, destination.remaining());
 | 
			
		||||
            var remaining = Math.min(entry.length - (int) position, destination.remaining());
 | 
			
		||||
            destination.put(backing, (int) position, remaining);
 | 
			
		||||
            position += remaining;
 | 
			
		||||
            return remaining;
 | 
			
		||||
@@ -285,7 +285,7 @@ public final class MemoryMount extends AbstractInMemoryMount<MemoryMount.FileEnt
 | 
			
		||||
 | 
			
		||||
        private byte[] ensureCapacity(int capacity) {
 | 
			
		||||
            var contents = Nullability.assertNonNull(entry.contents);
 | 
			
		||||
            if (capacity >= entry.length) {
 | 
			
		||||
            if (capacity >= contents.length) {
 | 
			
		||||
                var newCapacity = Math.max(capacity, contents.length << 1);
 | 
			
		||||
                contents = entry.contents = Arrays.copyOf(contents, newCapacity);
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,64 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.lua;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.lua.ILuaContext;
 | 
			
		||||
import dan200.computercraft.api.lua.LuaException;
 | 
			
		||||
import dan200.computercraft.api.lua.MethodResult;
 | 
			
		||||
import dan200.computercraft.core.Logging;
 | 
			
		||||
import dan200.computercraft.core.methods.LuaMethod;
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
import org.squiddev.cobalt.LuaError;
 | 
			
		||||
import org.squiddev.cobalt.LuaState;
 | 
			
		||||
import org.squiddev.cobalt.Varargs;
 | 
			
		||||
import org.squiddev.cobalt.function.VarArgFunction;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An "optimised" version of {@link ResultInterpreterFunction} which is guaranteed to never yield.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * As we never yield, we do not need to push a function to the stack, which removes a small amount of overhead.
 | 
			
		||||
 */
 | 
			
		||||
class BasicFunction extends VarArgFunction {
 | 
			
		||||
    private static final Logger LOG = LoggerFactory.getLogger(BasicFunction.class);
 | 
			
		||||
    private final CobaltLuaMachine machine;
 | 
			
		||||
    private final LuaMethod method;
 | 
			
		||||
    private final Object instance;
 | 
			
		||||
    private final ILuaContext context;
 | 
			
		||||
    private final String funcName;
 | 
			
		||||
 | 
			
		||||
    BasicFunction(CobaltLuaMachine machine, LuaMethod method, Object instance, ILuaContext context, String name) {
 | 
			
		||||
        this.machine = machine;
 | 
			
		||||
        this.method = method;
 | 
			
		||||
        this.instance = instance;
 | 
			
		||||
        this.context = context;
 | 
			
		||||
        funcName = name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Varargs invoke(LuaState luaState, Varargs args) throws LuaError {
 | 
			
		||||
        var arguments = VarargArguments.of(args);
 | 
			
		||||
        MethodResult results;
 | 
			
		||||
        try {
 | 
			
		||||
            results = method.apply(instance, context, arguments);
 | 
			
		||||
        } catch (LuaException e) {
 | 
			
		||||
            throw wrap(e);
 | 
			
		||||
        } catch (Throwable t) {
 | 
			
		||||
            LOG.error(Logging.JAVA_ERROR, "Error calling {} on {}", funcName, instance, t);
 | 
			
		||||
            throw new LuaError("Java Exception Thrown: " + t, 0);
 | 
			
		||||
        } finally {
 | 
			
		||||
            arguments.close();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (results.getCallback() != null) {
 | 
			
		||||
            throw new IllegalStateException("Cannot have a yielding non-yielding function");
 | 
			
		||||
        }
 | 
			
		||||
        return machine.toValues(results.getResult());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static LuaError wrap(LuaException exception) {
 | 
			
		||||
        return exception.hasLevel() ? new LuaError(exception.getMessage()) : new LuaError(exception.getMessage(), exception.getLevel());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -25,7 +25,6 @@ import org.squiddev.cobalt.lib.Bit32Lib;
 | 
			
		||||
import org.squiddev.cobalt.lib.CoreLibraries;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.io.InputStream;
 | 
			
		||||
import java.io.Serial;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
@@ -49,7 +48,7 @@ public class CobaltLuaMachine implements ILuaMachine {
 | 
			
		||||
 | 
			
		||||
    private @Nullable String eventFilter = null;
 | 
			
		||||
 | 
			
		||||
    public CobaltLuaMachine(MachineEnvironment environment, InputStream bios) throws MachineException, IOException {
 | 
			
		||||
    public CobaltLuaMachine(MachineEnvironment environment, InputStream bios) throws MachineException {
 | 
			
		||||
        timeout = environment.timeout();
 | 
			
		||||
        context = environment.context();
 | 
			
		||||
        luaMethods = environment.luaMethods();
 | 
			
		||||
@@ -81,7 +80,7 @@ public class CobaltLuaMachine implements ILuaMachine {
 | 
			
		||||
            globals.rawset("_CC_DEFAULT_SETTINGS", ValueFactory.valueOf(CoreConfig.defaultComputerSettings));
 | 
			
		||||
 | 
			
		||||
            // Add default APIs
 | 
			
		||||
            for (var api : environment.apis()) addAPI(globals, api);
 | 
			
		||||
            for (var api : environment.apis()) addAPI(state, globals, api);
 | 
			
		||||
 | 
			
		||||
            // And load the BIOS
 | 
			
		||||
            var value = LoadState.load(state, bios, "@bios.lua", globals);
 | 
			
		||||
@@ -93,7 +92,7 @@ public class CobaltLuaMachine implements ILuaMachine {
 | 
			
		||||
        timeout.addListener(timeoutListener);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void addAPI(LuaTable globals, ILuaAPI api) {
 | 
			
		||||
    private void addAPI(LuaState state, LuaTable globals, ILuaAPI api) throws LuaError {
 | 
			
		||||
        // Add the methods of an API to the global table
 | 
			
		||||
        var table = wrapLuaObject(api);
 | 
			
		||||
        if (table == null) {
 | 
			
		||||
@@ -103,6 +102,9 @@ public class CobaltLuaMachine implements ILuaMachine {
 | 
			
		||||
 | 
			
		||||
        var names = api.getNames();
 | 
			
		||||
        for (var name : names) globals.rawset(name, table);
 | 
			
		||||
 | 
			
		||||
        var moduleName = api.getModuleName();
 | 
			
		||||
        if (moduleName != null) state.registry().getSubTable(Constants.LOADED).rawset(moduleName, table);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void updateTimeout() {
 | 
			
		||||
@@ -164,9 +166,7 @@ public class CobaltLuaMachine implements ILuaMachine {
 | 
			
		||||
    private LuaTable wrapLuaObject(Object object) {
 | 
			
		||||
        var table = new LuaTable();
 | 
			
		||||
        var found = luaMethods.forEachMethod(object, (target, name, method, info) ->
 | 
			
		||||
            table.rawset(name, info != null && info.nonYielding()
 | 
			
		||||
                ? new BasicFunction(this, method, target, context, name)
 | 
			
		||||
                : new ResultInterpreterFunction(this, method, target, context, name)));
 | 
			
		||||
            table.rawset(name, new ResultInterpreterFunction(this, method, target, context, name)));
 | 
			
		||||
 | 
			
		||||
        return found ? table : null;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -73,7 +73,7 @@ class ResultInterpreterFunction extends ResumableVarArgFunction<ResultInterprete
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected Varargs resumeThis(LuaState state, Container container, Varargs args) throws LuaError, UnwindThrowable {
 | 
			
		||||
    public Varargs resume(LuaState state, Container container, Varargs args) throws LuaError, UnwindThrowable {
 | 
			
		||||
        MethodResult results;
 | 
			
		||||
        var arguments = CobaltLuaMachine.toObjects(args);
 | 
			
		||||
        try {
 | 
			
		||||
@@ -98,6 +98,6 @@ class ResultInterpreterFunction extends ResumableVarArgFunction<ResultInterprete
 | 
			
		||||
        if (!exception.hasLevel() && adjust == 0) return new LuaError(exception.getMessage());
 | 
			
		||||
 | 
			
		||||
        var level = exception.getLevel();
 | 
			
		||||
        return new LuaError(exception.getMessage(), level <= 0 ? level : level + adjust + 1);
 | 
			
		||||
        return new LuaError(exception.getMessage(), level <= 0 ? level : level + adjust);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,32 @@
 | 
			
		||||
# New features in CC: Tweaked 1.109.6
 | 
			
		||||
 | 
			
		||||
* Improve several Lua parser error messages.
 | 
			
		||||
* Allow addon mods to register `require`able modules.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix weak tables becoming malformed when keys are GCed.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.109.5
 | 
			
		||||
 | 
			
		||||
* Add a new `/computercraft-computer-folder` command to open a computer's folder
 | 
			
		||||
  in singleplayer.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Discard characters being typed into the editor when closing `edit`'s `Run` screen.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.109.4
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Don't log warnings when a computer allocates no bytes.
 | 
			
		||||
* Fix incorrect list index in command computer's NBT conversion (lonevox).
 | 
			
		||||
* Fix `endPage()` not updating the printer's block state.
 | 
			
		||||
* Several documentation improvements (znepb).
 | 
			
		||||
* Correctly mount disks before computer startup, not afterwards.
 | 
			
		||||
* Update to Cobalt 0.9
 | 
			
		||||
  * Debug hooks are now correctly called for every function.
 | 
			
		||||
  * Fix several minor inconsistencies with `debug.getinfo`.
 | 
			
		||||
  * Fix Lua tables being sized incorrectly when created from varargs.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.109.3
 | 
			
		||||
 | 
			
		||||
* Command computers now display in the operator items creative tab.
 | 
			
		||||
@@ -6,7 +35,7 @@ Several bug fixes:
 | 
			
		||||
* Error if too many websocket messages are queued to be sent at once.
 | 
			
		||||
* Fix trailing-comma on method calls (e.g. `x:f(a, )` not using our custom error message.
 | 
			
		||||
* Fix internal compiler error when using `goto` as the first statement in an `if` block.
 | 
			
		||||
* Fix incorrect incorrect resizing of a tables' hash part when adding and removing keys.
 | 
			
		||||
* Fix incorrect resizing of a tables' hash part when adding and removing keys.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.109.2
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,9 @@
 | 
			
		||||
New features in CC: Tweaked 1.109.3
 | 
			
		||||
New features in CC: Tweaked 1.109.6
 | 
			
		||||
 | 
			
		||||
* Command computers now display in the operator items creative tab.
 | 
			
		||||
* Improve several Lua parser error messages.
 | 
			
		||||
* Allow addon mods to register `require`able modules.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Error if too many websocket messages are queued to be sent at once.
 | 
			
		||||
* Fix trailing-comma on method calls (e.g. `x:f(a, )` not using our custom error message.
 | 
			
		||||
* Fix internal compiler error when using `goto` as the first statement in an `if` block.
 | 
			
		||||
* Fix incorrect incorrect resizing of a tables' hash part when adding and removing keys.
 | 
			
		||||
* Fix weak tables becoming malformed when keys are GCed.
 | 
			
		||||
 | 
			
		||||
Type "help changelog" to see the full version history.
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,37 @@
 | 
			
		||||
-- SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
 | 
			
		||||
--
 | 
			
		||||
-- SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
--[[- Utilities for working with events.
 | 
			
		||||
 | 
			
		||||
> [!DANGER]
 | 
			
		||||
> This is an internal module and SHOULD NOT be used in your own code. It may
 | 
			
		||||
> be removed or changed at any time.
 | 
			
		||||
 | 
			
		||||
@local
 | 
			
		||||
]]
 | 
			
		||||
 | 
			
		||||
--[[-
 | 
			
		||||
Attempt to discard a [`event!char`] event that may follow a [`event!key`] event.
 | 
			
		||||
 | 
			
		||||
This attempts to flush the event queue via a timer, stopping early if we observe
 | 
			
		||||
another key or char event.
 | 
			
		||||
 | 
			
		||||
We flush the event queue by waiting a single tick. It is technically possible
 | 
			
		||||
the key and char events will be delivered in different ticks, but it should be
 | 
			
		||||
very rare, and not worth adding extra delay for.
 | 
			
		||||
]]
 | 
			
		||||
local function discard_char()
 | 
			
		||||
    local timer = os.startTimer(0)
 | 
			
		||||
    while true do
 | 
			
		||||
        local event, id = os.pullEvent()
 | 
			
		||||
        if event == "timer" and id == timer then break
 | 
			
		||||
        elseif event == "char" or event == "key" or event == "key_up" then
 | 
			
		||||
            os.cancelTimer(timer)
 | 
			
		||||
            break
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
return { discard_char = discard_char }
 | 
			
		||||
@@ -453,32 +453,53 @@ function errors.local_function_dot(local_start, local_end, dot_start, dot_end)
 | 
			
		||||
    }
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- A statement of the form `x.y z`
 | 
			
		||||
--[[- A statement of the form `x.y`
 | 
			
		||||
 | 
			
		||||
@tparam number token The token id.
 | 
			
		||||
@tparam number pos The position right after this name.
 | 
			
		||||
@return The resulting parse error.
 | 
			
		||||
]]
 | 
			
		||||
function errors.standalone_name(pos)
 | 
			
		||||
    expect(1, pos, "number")
 | 
			
		||||
function errors.standalone_name(token, pos)
 | 
			
		||||
    expect(1, token, "number")
 | 
			
		||||
    expect(2, pos, "number")
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        "Unexpected symbol after name.",
 | 
			
		||||
        "Unexpected " .. token_names[token] .. " after name.",
 | 
			
		||||
        annotate(pos),
 | 
			
		||||
        "Did you mean to assign this or call it as a function?",
 | 
			
		||||
    }
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- A statement of the form `x.y, z`
 | 
			
		||||
 | 
			
		||||
@tparam number token The token id.
 | 
			
		||||
@tparam number pos The position right after this name.
 | 
			
		||||
@return The resulting parse error.
 | 
			
		||||
]]
 | 
			
		||||
function errors.standalone_names(token, pos)
 | 
			
		||||
    expect(1, token, "number")
 | 
			
		||||
    expect(2, pos, "number")
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        "Unexpected " .. token_names[token] .. " after name.",
 | 
			
		||||
        annotate(pos),
 | 
			
		||||
        "Did you mean to assign this?",
 | 
			
		||||
    }
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- A statement of the form `x.y`. This is similar to [`standalone_name`], but
 | 
			
		||||
when the next token is on another line.
 | 
			
		||||
 | 
			
		||||
@tparam number token The token id.
 | 
			
		||||
@tparam number pos The position right after this name.
 | 
			
		||||
@return The resulting parse error.
 | 
			
		||||
]]
 | 
			
		||||
function errors.standalone_name_call(pos)
 | 
			
		||||
    expect(1, pos, "number")
 | 
			
		||||
function errors.standalone_name_call(token, pos)
 | 
			
		||||
    expect(1, token, "number")
 | 
			
		||||
    expect(2, pos, "number")
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        "Unexpected symbol after variable.",
 | 
			
		||||
        "Unexpected " .. token_names[token] .. " after name.",
 | 
			
		||||
        annotate(pos + 1, "Expected something before the end of the line."),
 | 
			
		||||
        "Tip: Use " .. code("()") .. " to call with no arguments.",
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -124,13 +124,22 @@ local function make_package(env, dir)
 | 
			
		||||
    local package = {}
 | 
			
		||||
    package.loaded = {
 | 
			
		||||
        _G = _G,
 | 
			
		||||
        bit32 = bit32,
 | 
			
		||||
        coroutine = coroutine,
 | 
			
		||||
        math = math,
 | 
			
		||||
        package = package,
 | 
			
		||||
        string = string,
 | 
			
		||||
        table = table,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    -- Copy everything from the global package table to this instance.
 | 
			
		||||
    --
 | 
			
		||||
    -- This table is an internal implementation detail - it is NOT intended to
 | 
			
		||||
    -- be extended by user code.
 | 
			
		||||
    local registry = debug.getregistry()
 | 
			
		||||
    if registry and type(registry._LOADED) == "table" then
 | 
			
		||||
        for k, v in next, registry._LOADED do
 | 
			
		||||
            if type(k) == "string" then
 | 
			
		||||
                package.loaded[k] = v
 | 
			
		||||
            end
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    package.path = "?;?.lua;?/init.lua;/rom/modules/main/?;/rom/modules/main/?.lua;/rom/modules/main/?/init.lua"
 | 
			
		||||
    if turtle then
 | 
			
		||||
        package.path = package.path .. ";/rom/modules/turtle/?;/rom/modules/turtle/?.lua;/rom/modules/turtle/?/init.lua"
 | 
			
		||||
 
 | 
			
		||||
@@ -88,6 +88,7 @@ for i = 1, #wrapped do
 | 
			
		||||
    term.write(wrapped[i])
 | 
			
		||||
end
 | 
			
		||||
os.pullEvent('key')
 | 
			
		||||
require "cc.internal.event".discard_char()
 | 
			
		||||
]]
 | 
			
		||||
 | 
			
		||||
-- Menus
 | 
			
		||||
 
 | 
			
		||||
@@ -300,7 +300,7 @@ local menu_choices = {
 | 
			
		||||
        return false
 | 
			
		||||
    end,
 | 
			
		||||
    Exit = function()
 | 
			
		||||
        sleep(0) -- Super janky, but consumes stray "char" events from pressing Ctrl then E separately.
 | 
			
		||||
        require "cc.internal.event".discard_char() -- Consume stray "char" events from pressing Ctrl then E separately.
 | 
			
		||||
        return true
 | 
			
		||||
    end,
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -257,7 +257,7 @@ while true do
 | 
			
		||||
            offset = print_height - content_height
 | 
			
		||||
            draw()
 | 
			
		||||
        elseif param == keys.q then
 | 
			
		||||
            sleep(0) -- Super janky, but consumes stray "char" events.
 | 
			
		||||
            require "cc.internal.event".discard_char()
 | 
			
		||||
            break
 | 
			
		||||
        end
 | 
			
		||||
    elseif event == "mouse_scroll" then
 | 
			
		||||
 
 | 
			
		||||
@@ -67,8 +67,8 @@ public class MethodTest {
 | 
			
		||||
    public void testPeripheralThrow() {
 | 
			
		||||
        ComputerBootstrap.run(
 | 
			
		||||
            "local throw = peripheral.wrap('top')\n" +
 | 
			
		||||
                "local _, err = pcall(throw.thisThread) assert(err == 'pcall: !', err)\n" +
 | 
			
		||||
                "local _, err = pcall(throw.mainThread) assert(err == 'pcall: !', err)",
 | 
			
		||||
                "local _, err = pcall(function() throw.thisThread() end) assert(err == '/test.lua:2: !', (\"thisThread: %q\"):format(err))\n" +
 | 
			
		||||
                "local _, err = pcall(function() throw.mainThread() end) assert(err == '/test.lua:3: !', (\"mainThread: %q\"):format(err))\n",
 | 
			
		||||
            x -> x.getEnvironment().setPeripheral(ComputerSide.TOP, new PeripheralThrow()),
 | 
			
		||||
            50
 | 
			
		||||
        );
 | 
			
		||||
@@ -89,6 +89,16 @@ public class MethodTest {
 | 
			
		||||
            x -> x.addApi(new ReturnFunction()), 50);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testModule() {
 | 
			
		||||
        ComputerBootstrap.run(
 | 
			
		||||
            """
 | 
			
		||||
            assert(require "test.module".func() == 123)
 | 
			
		||||
            """,
 | 
			
		||||
            x -> x.addApi(new IsModule()), 50);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static class MainThread implements ILuaAPI, IPeripheral {
 | 
			
		||||
        public final String thread = Thread.currentThread().getName();
 | 
			
		||||
 | 
			
		||||
@@ -206,7 +216,7 @@ public class MethodTest {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public MethodResult callMethod(ILuaContext context, int method, IArguments arguments) throws LuaException {
 | 
			
		||||
        public MethodResult callMethod(ILuaContext context, int method, IArguments arguments) {
 | 
			
		||||
            return MethodResult.of();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -227,4 +237,21 @@ public class MethodTest {
 | 
			
		||||
            return new String[]{ "func" };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static class IsModule implements ILuaAPI {
 | 
			
		||||
        @Override
 | 
			
		||||
        public String[] getNames() {
 | 
			
		||||
            return new String[0];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public @Nullable String getModuleName() {
 | 
			
		||||
            return "test.module";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @LuaFunction
 | 
			
		||||
        public final int func() {
 | 
			
		||||
            return 123;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ import dan200.computercraft.core.computer.mainthread.MainThreadConfig;
 | 
			
		||||
import dan200.computercraft.core.filesystem.MemoryMount;
 | 
			
		||||
import dan200.computercraft.core.terminal.Terminal;
 | 
			
		||||
import dan200.computercraft.test.core.computer.BasicEnvironment;
 | 
			
		||||
import org.intellij.lang.annotations.Language;
 | 
			
		||||
import org.junit.jupiter.api.Assertions;
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
@@ -31,7 +32,7 @@ public class ComputerBootstrap {
 | 
			
		||||
    private static final int TPS = 20;
 | 
			
		||||
    public static final int MAX_TIME = 10;
 | 
			
		||||
 | 
			
		||||
    public static void run(String program, Consumer<Computer> setup, int maxTimes) {
 | 
			
		||||
    public static void run(@Language("lua") String program, Consumer<Computer> setup, int maxTimes) {
 | 
			
		||||
        var mount = new MemoryMount()
 | 
			
		||||
            .addFile("test.lua", program)
 | 
			
		||||
            .addFile("startup.lua", "assertion.assert(pcall(loadfile('test.lua', nil, _ENV))) os.shutdown()");
 | 
			
		||||
@@ -39,7 +40,7 @@ public class ComputerBootstrap {
 | 
			
		||||
        run(mount, setup, maxTimes);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void run(String program, int maxTimes) {
 | 
			
		||||
    public static void run(@Language("lua") String program, int maxTimes) {
 | 
			
		||||
        run(program, x -> {
 | 
			
		||||
        }, maxTimes);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,43 @@
 | 
			
		||||
-- SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
--
 | 
			
		||||
-- SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
local timeout = require "test_helpers".timeout
 | 
			
		||||
 | 
			
		||||
describe("cc.internal.event", function()
 | 
			
		||||
    local event = require "cc.internal.event"
 | 
			
		||||
    describe("discard_char", function()
 | 
			
		||||
 | 
			
		||||
        local function test(events)
 | 
			
		||||
            local unique_event = "flush_" .. math.random(2 ^ 30)
 | 
			
		||||
 | 
			
		||||
            -- Queue and pull to flush the queue once.
 | 
			
		||||
            os.queueEvent(unique_event)
 | 
			
		||||
            os.pullEvent(unique_event)
 | 
			
		||||
 | 
			
		||||
            -- Queue our desired events
 | 
			
		||||
            for i = 1, #events do os.queueEvent(table.unpack(events[i])) end
 | 
			
		||||
 | 
			
		||||
            timeout(0.1, function()
 | 
			
		||||
                event.discard_char()
 | 
			
		||||
 | 
			
		||||
                -- Then read the remainder of the event queue, and check there's
 | 
			
		||||
                -- no char event.
 | 
			
		||||
                os.queueEvent(unique_event)
 | 
			
		||||
                while true do
 | 
			
		||||
                    local event = os.pullEvent()
 | 
			
		||||
                    if event == unique_event then break end
 | 
			
		||||
                    expect(event):ne("char")
 | 
			
		||||
                end
 | 
			
		||||
            end)
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        it("discards char events", function()
 | 
			
		||||
            test { { "char", "a" } }
 | 
			
		||||
        end)
 | 
			
		||||
 | 
			
		||||
        it("handles an empty event queue", function()
 | 
			
		||||
            test {}
 | 
			
		||||
        end)
 | 
			
		||||
    end)
 | 
			
		||||
end)
 | 
			
		||||
@@ -347,7 +347,7 @@ function ( xyz , while
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```txt
 | 
			
		||||
Unexpected while.
 | 
			
		||||
Unexpected while. Expected a variable name.
 | 
			
		||||
   |
 | 
			
		||||
 1 | function ( xyz , while
 | 
			
		||||
   |                  ^^^^^
 | 
			
		||||
@@ -483,11 +483,11 @@ xyz , xyz while
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```txt
 | 
			
		||||
Unexpected symbol after name.
 | 
			
		||||
Unexpected while after name.
 | 
			
		||||
   |
 | 
			
		||||
 1 | xyz , xyz while
 | 
			
		||||
   |           ^
 | 
			
		||||
Did you mean to assign this or call it as a function?
 | 
			
		||||
Did you mean to assign this?
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -831,7 +831,7 @@ xyz while
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```txt
 | 
			
		||||
Unexpected symbol after name.
 | 
			
		||||
Unexpected while after name.
 | 
			
		||||
   |
 | 
			
		||||
 1 | xyz while
 | 
			
		||||
   |     ^
 | 
			
		||||
@@ -858,7 +858,7 @@ xyz while
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```txt
 | 
			
		||||
Unexpected symbol after name.
 | 
			
		||||
Unexpected while after name.
 | 
			
		||||
   |
 | 
			
		||||
 1 | xyz while
 | 
			
		||||
   |     ^
 | 
			
		||||
@@ -1056,7 +1056,7 @@ local while
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```txt
 | 
			
		||||
Unexpected while.
 | 
			
		||||
Unexpected while. Expected a variable name.
 | 
			
		||||
   |
 | 
			
		||||
 1 | local while
 | 
			
		||||
   |       ^^^^^
 | 
			
		||||
@@ -1272,7 +1272,7 @@ repeat --[[eof]]
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```txt
 | 
			
		||||
Unexpected end of file. Expected a statement.
 | 
			
		||||
Unexpected end of file. Expected a variable name.
 | 
			
		||||
   |
 | 
			
		||||
 2 | -- Line 1: 'until' expected near <eof> (program)
 | 
			
		||||
   |                                                 ^
 | 
			
		||||
@@ -1389,7 +1389,7 @@ while
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```txt
 | 
			
		||||
Unexpected while.
 | 
			
		||||
Unexpected while. Expected an expression.
 | 
			
		||||
   |
 | 
			
		||||
 1 | while
 | 
			
		||||
   | ^^^^^
 | 
			
		||||
 
 | 
			
		||||
@@ -38,6 +38,20 @@ Unexpected = in expression.
 | 
			
		||||
Tip: Wrap the preceding expression in [ and ] to use it as a table key.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
and also
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
return { x + 1 = 1 }
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```txt
 | 
			
		||||
Unexpected = in expression.
 | 
			
		||||
   |
 | 
			
		||||
 1 | return { x + 1 = 1 }
 | 
			
		||||
   |                ^
 | 
			
		||||
Tip: Wrap the preceding expression in [ and ] to use it as a table key.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Note this doesn't occur if this there's already a table key here:
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
@@ -102,6 +116,7 @@ Unexpected end of file. Are you missing a closing bracket?
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Missing commas in tables
 | 
			
		||||
We try to detect missing commas in tables, and print an appropriate error message.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
return { 1 2 }
 | 
			
		||||
@@ -129,6 +144,39 @@ Unexpected number in table.
 | 
			
		||||
 1 | return { 1, 2 3 }
 | 
			
		||||
   |              ^ Are you missing a comma here?
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
This also works with table keys.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
print({ x = 1 y = 2 })
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```txt
 | 
			
		||||
Unexpected identifier in table.
 | 
			
		||||
   |
 | 
			
		||||
 1 | print({ x = 1 y = 2 })
 | 
			
		||||
   |               ^
 | 
			
		||||
   |
 | 
			
		||||
 1 | print({ x = 1 y = 2 })
 | 
			
		||||
   |              ^ Are you missing a comma here?
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
print({ ["x"] = 1 ["y"] = 2 })
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```txt
 | 
			
		||||
Unexpected [ in table.
 | 
			
		||||
   |
 | 
			
		||||
 1 | print({ ["x"] = 1 ["y"] = 2 })
 | 
			
		||||
   |                   ^
 | 
			
		||||
   |
 | 
			
		||||
 1 | print({ ["x"] = 1 ["y"] = 2 })
 | 
			
		||||
   |                  ^ Are you missing a comma here?
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
We gracefully handle the case where we are actually missing a closing brace.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
print({ 1, )
 | 
			
		||||
```
 | 
			
		||||
@@ -172,7 +220,7 @@ local _ = 1
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```txt
 | 
			
		||||
Unexpected symbol after variable.
 | 
			
		||||
Unexpected local after name.
 | 
			
		||||
   |
 | 
			
		||||
 1 | term.clear
 | 
			
		||||
   |           ^ Expected something before the end of the line.
 | 
			
		||||
@@ -186,7 +234,7 @@ x 1
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```txt
 | 
			
		||||
Unexpected symbol after name.
 | 
			
		||||
Unexpected number after name.
 | 
			
		||||
   |
 | 
			
		||||
 1 | x 1
 | 
			
		||||
   |   ^
 | 
			
		||||
@@ -200,13 +248,41 @@ term.clear
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```txt
 | 
			
		||||
Unexpected symbol after variable.
 | 
			
		||||
Unexpected end of file after name.
 | 
			
		||||
   |
 | 
			
		||||
 1 | term.clear
 | 
			
		||||
   |           ^ Expected something before the end of the line.
 | 
			
		||||
Tip: Use () to call with no arguments.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
When we've got a list of variables, we only suggest assigning it.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
term.clear, foo
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```txt
 | 
			
		||||
Unexpected end of file after name.
 | 
			
		||||
   |
 | 
			
		||||
 1 | term.clear, foo
 | 
			
		||||
   |                ^
 | 
			
		||||
Did you mean to assign this?
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
And when we've got a partial expression, we only suggest calling it.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
(a + b)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```txt
 | 
			
		||||
Unexpected end of file after name.
 | 
			
		||||
   |
 | 
			
		||||
 1 | (a + b)
 | 
			
		||||
   |        ^ Expected something before the end of the line.
 | 
			
		||||
Tip: Use () to call with no arguments.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## If statements
 | 
			
		||||
For if statements, we say when we expected the `then` keyword.
 | 
			
		||||
 | 
			
		||||
@@ -425,7 +501,7 @@ goto 2
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```txt
 | 
			
		||||
Unexpected symbol after name.
 | 
			
		||||
Unexpected number after name.
 | 
			
		||||
   |
 | 
			
		||||
 1 | goto 2
 | 
			
		||||
   |      ^
 | 
			
		||||
@@ -460,6 +536,31 @@ Unexpected end of file.
 | 
			
		||||
   |   ^
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Missing function arguments
 | 
			
		||||
We provide an error message for missing arguments in function definitions:
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
function f
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```txt
 | 
			
		||||
Unexpected end of file. Expected ( to start function arguments.
 | 
			
		||||
   |
 | 
			
		||||
 1 | function f
 | 
			
		||||
   |           ^
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
return function
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```txt
 | 
			
		||||
Unexpected end of file. Expected ( to start function arguments.
 | 
			
		||||
   |
 | 
			
		||||
 1 | return function
 | 
			
		||||
   |                ^
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
# Function calls
 | 
			
		||||
 | 
			
		||||
## Additional commas
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user