mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-11-04 07:32:59 +00:00 
			
		
		
		
	Compare commits
	
		
			67 Commits
		
	
	
		
			v1.20.1-1.
			...
			v1.20.1-1.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					3493159a05 | ||
| 
						 | 
					eead67e314 | ||
| 
						 | 
					3b8813cf8f | ||
| 
						 | 
					a9191a4d4e | ||
| 
						 | 
					451a2593ce | ||
| 
						 | 
					d38b1da974 | ||
| 
						 | 
					6e374579a4 | ||
| 
						 | 
					4daa2a2b6a | ||
| 
						 | 
					84b6edab82 | ||
| 
						 | 
					31aaf46d09 | ||
| 
						 | 
					2d11b51c62 | ||
| 
						 | 
					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 | ||
| 
						 | 
					8be6b1b772 | ||
| 
						 | 
					104d5e70de | ||
| 
						 | 
					e3bda2f763 | ||
| 
						 | 
					234f69e8e5 | ||
| 
						 | 
					ed3a17f9b9 | ||
| 
						 | 
					0349c2b1f9 | ||
| 
						 | 
					03f9e6bd6d | ||
| 
						 | 
					9d8c933a14 | ||
| 
						 | 
					78bb3da58c | ||
| 
						 | 
					39a5e40c92 | ||
| 
						 | 
					763ba51919 | ||
| 
						 | 
					cf6ec8c28f | ||
| 
						 | 
					95d3b646b2 | ||
| 
						 | 
					488f66eead | ||
| 
						 | 
					1f7d245876 | ||
| 
						 | 
					af12b3a0ea | ||
| 
						 | 
					eb3e8ba677 | ||
| 
						 | 
					2043939531 | ||
| 
						 | 
					84a799d27a | ||
| 
						 | 
					fe826f5c9c | ||
| 
						 | 
					f8b7422294 | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -7,7 +7,9 @@
 | 
			
		||||
/logs
 | 
			
		||||
/build
 | 
			
		||||
/projects/*/logs
 | 
			
		||||
/projects/fabric/fabricloader.log
 | 
			
		||||
/projects/*/build
 | 
			
		||||
/projects/*/src/test/generated_tests/
 | 
			
		||||
/buildSrc/build
 | 
			
		||||
/out
 | 
			
		||||
/buildSrc/out
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,6 @@ repositories {
 | 
			
		||||
    url "https://squiddev.cc/maven/"
 | 
			
		||||
    content {
 | 
			
		||||
      includeGroup("cc.tweaked")
 | 
			
		||||
      includeModule("org.squiddev", "Cobalt")
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -76,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/).
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,8 @@ plugins {
 | 
			
		||||
    publishing
 | 
			
		||||
    alias(libs.plugins.taskTree)
 | 
			
		||||
    alias(libs.plugins.githubRelease)
 | 
			
		||||
    alias(libs.plugins.gradleVersions)
 | 
			
		||||
    alias(libs.plugins.versionCatalogUpdate)
 | 
			
		||||
    id("org.jetbrains.gradle.plugin.idea-ext")
 | 
			
		||||
    id("cc-tweaked")
 | 
			
		||||
}
 | 
			
		||||
@@ -102,3 +104,9 @@ idea.project.settings.compiler.javac {
 | 
			
		||||
        }
 | 
			
		||||
        .toMap()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
versionCatalogUpdate {
 | 
			
		||||
    sortByKey.set(false)
 | 
			
		||||
    pin { versions.addAll("fastutil", "guava", "netty", "slf4j") }
 | 
			
		||||
    keep { keepUnusedLibraries.set(true) }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,8 @@
 | 
			
		||||
plugins {
 | 
			
		||||
    `java-gradle-plugin`
 | 
			
		||||
    `kotlin-dsl`
 | 
			
		||||
    alias(libs.plugins.gradleVersions)
 | 
			
		||||
    alias(libs.plugins.versionCatalogUpdate)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Duplicated in settings.gradle.kts
 | 
			
		||||
@@ -27,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 {
 | 
			
		||||
@@ -53,8 +55,7 @@ dependencies {
 | 
			
		||||
    implementation(libs.ideaExt)
 | 
			
		||||
    implementation(libs.librarian)
 | 
			
		||||
    implementation(libs.minotaur)
 | 
			
		||||
    implementation(libs.vanillaGradle)
 | 
			
		||||
    implementation(libs.vineflower)
 | 
			
		||||
    implementation(libs.vanillaExtract)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
gradlePlugin {
 | 
			
		||||
@@ -75,3 +76,9 @@ gradlePlugin {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
versionCatalogUpdate {
 | 
			
		||||
    sortByKey.set(false)
 | 
			
		||||
    keep { keepUnusedLibraries.set(true) }
 | 
			
		||||
    catalogFile.set(file("../gradle/libs.versions.toml"))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -76,6 +76,12 @@ dependencies {
 | 
			
		||||
    val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
 | 
			
		||||
    checkstyle(libs.findLibrary("checkstyle").get())
 | 
			
		||||
 | 
			
		||||
    constraints {
 | 
			
		||||
        checkstyle("org.codehaus.plexus:plexus-container-default:2.1.1") {
 | 
			
		||||
            because("2.1.0 depends on deprecated Google collections module")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    errorprone(libs.findLibrary("errorProne-core").get())
 | 
			
		||||
    errorprone(libs.findLibrary("nullAway").get())
 | 
			
		||||
}
 | 
			
		||||
@@ -201,6 +207,7 @@ spotless {
 | 
			
		||||
    val ktlintConfig = mapOf(
 | 
			
		||||
        "ktlint_standard_no-wildcard-imports" to "disabled",
 | 
			
		||||
        "ktlint_standard_class-naming" to "disabled",
 | 
			
		||||
        "ktlint_standard_function-naming" to "disabled",
 | 
			
		||||
        "ij_kotlin_allow_trailing_comma" to "true",
 | 
			
		||||
        "ij_kotlin_allow_trailing_comma_on_call_site" to "true",
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -10,9 +10,11 @@ import org.gradle.api.GradleException
 | 
			
		||||
import org.gradle.api.NamedDomainObjectProvider
 | 
			
		||||
import org.gradle.api.Project
 | 
			
		||||
import org.gradle.api.Task
 | 
			
		||||
import org.gradle.api.artifacts.Dependency
 | 
			
		||||
import org.gradle.api.attributes.TestSuiteType
 | 
			
		||||
import org.gradle.api.file.FileSystemOperations
 | 
			
		||||
import org.gradle.api.plugins.JavaPluginExtension
 | 
			
		||||
import org.gradle.api.provider.ListProperty
 | 
			
		||||
import org.gradle.api.provider.Provider
 | 
			
		||||
import org.gradle.api.provider.SetProperty
 | 
			
		||||
import org.gradle.api.reporting.ReportingExtension
 | 
			
		||||
@@ -73,11 +75,17 @@ abstract class CCTweakedExtension(
 | 
			
		||||
     */
 | 
			
		||||
    val sourceDirectories: SetProperty<SourceSetReference> = project.objects.setProperty(SourceSetReference::class.java)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Dependencies excluded from published artifacts.
 | 
			
		||||
     */
 | 
			
		||||
    private val excludedDeps: ListProperty<Dependency> = project.objects.listProperty(Dependency::class.java)
 | 
			
		||||
 | 
			
		||||
    /** All source sets referenced by this project. */
 | 
			
		||||
    val sourceSets = sourceDirectories.map { x -> x.map { it.sourceSet } }
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        sourceDirectories.finalizeValueOnRead()
 | 
			
		||||
        excludedDeps.finalizeValueOnRead()
 | 
			
		||||
        project.afterEvaluate { sourceDirectories.disallowChanges() }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -246,6 +254,20 @@ abstract class CCTweakedExtension(
 | 
			
		||||
        ).resolve().single()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Exclude a dependency from being published in Maven.
 | 
			
		||||
     */
 | 
			
		||||
    fun exclude(dep: Dependency) {
 | 
			
		||||
        excludedDeps.add(dep)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Configure a [MavenDependencySpec].
 | 
			
		||||
     */
 | 
			
		||||
    fun configureExcludes(spec: MavenDependencySpec) {
 | 
			
		||||
        for (dep in excludedDeps.get()) spec.exclude(dep)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        private val COMMIT_COUNTS = Pattern.compile("""^\s*[0-9]+\s+(.*)$""")
 | 
			
		||||
        private val IGNORED_USERS = setOf(
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,92 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
    fun run() {
 | 
			
		||||
        var ok = true
 | 
			
		||||
        for (configuration in configuration.get()) {
 | 
			
		||||
            configuration.incoming.resolutionResult.allDependencies {
 | 
			
		||||
                if (!check(this@allDependencies)) ok = false
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!ok) {
 | 
			
		||||
            throw GradleException("Mismatched versions in Minecraft dependencies. gradle/libs.versions.toml may need updating.")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun check(dependency: DependencyResult): Boolean {
 | 
			
		||||
        if (dependency !is ResolvedDependencyResult) {
 | 
			
		||||
            logger.warn("Found unexpected dependency result {}", dependency)
 | 
			
		||||
            return false
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Skip dependencies on non-modules.
 | 
			
		||||
        val requested = dependency.requested
 | 
			
		||||
        if (requested !is ModuleComponentSelector) return true
 | 
			
		||||
 | 
			
		||||
        // If this dependency is specified within some project (so is non-transitive), or is pulled in via Minecraft,
 | 
			
		||||
        // then check for consistency.
 | 
			
		||||
        // It would be nice to be smarter about transitive dependencies, but avoiding false positives is hard.
 | 
			
		||||
        val from = dependency.from.id
 | 
			
		||||
        if (
 | 
			
		||||
            from is ProjectComponentIdentifier ||
 | 
			
		||||
            from is ModuleComponentIdentifier && (from.group == "net.minecraft" || from.group == "io.netty")
 | 
			
		||||
        ) {
 | 
			
		||||
            // If the version is different between the requested and selected version, report an error.
 | 
			
		||||
            val selected = dependency.selected.moduleVersion!!.version
 | 
			
		||||
            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
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,7 +6,6 @@ package cc.tweaked.gradle
 | 
			
		||||
 | 
			
		||||
import org.gradle.api.artifacts.dsl.DependencyHandler
 | 
			
		||||
import org.gradle.api.file.FileSystemLocation
 | 
			
		||||
import org.gradle.api.file.FileSystemLocationProperty
 | 
			
		||||
import org.gradle.api.provider.Property
 | 
			
		||||
import org.gradle.api.provider.Provider
 | 
			
		||||
import org.gradle.api.tasks.JavaExec
 | 
			
		||||
@@ -129,3 +128,30 @@ fun <T> Property<T>.setProvider(provider: Provider<out T>) = set(provider)
 | 
			
		||||
 | 
			
		||||
/** Short-cut method to get the absolute path of a [FileSystemLocation] provider. */
 | 
			
		||||
fun Provider<out FileSystemLocation>.getAbsolutePath(): String = get().asFile.absolutePath
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get the version immediately after the provided version.
 | 
			
		||||
 *
 | 
			
		||||
 * For example, given "1.2.3", this will return "1.2.4".
 | 
			
		||||
 */
 | 
			
		||||
fun getNextVersion(version: String): String {
 | 
			
		||||
    // Split a version like x.y.z-SNAPSHOT into x.y.z and -SNAPSHOT
 | 
			
		||||
    val dashIndex = version.indexOf('-')
 | 
			
		||||
    val mainVersion = if (dashIndex < 0) version else version.substring(0, dashIndex)
 | 
			
		||||
 | 
			
		||||
    // Find the last component in x.y.z and increment it.
 | 
			
		||||
    val lastIndex = mainVersion.lastIndexOf('.')
 | 
			
		||||
    if (lastIndex < 0) throw IllegalArgumentException("Cannot parse version format \"$version\"")
 | 
			
		||||
    val lastVersion = try {
 | 
			
		||||
        version.substring(lastIndex + 1).toInt()
 | 
			
		||||
    } catch (e: NumberFormatException) {
 | 
			
		||||
        throw IllegalArgumentException("Cannot parse version format \"$version\"", e)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Then append all components together.
 | 
			
		||||
    val out = StringBuilder()
 | 
			
		||||
    out.append(version, 0, lastIndex + 1)
 | 
			
		||||
    out.append(lastVersion + 1)
 | 
			
		||||
    if (dashIndex >= 0) out.append(version, dashIndex, version.length)
 | 
			
		||||
    return out.toString()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,8 @@ package cc.tweaked.gradle
 | 
			
		||||
 | 
			
		||||
import org.gradle.api.artifacts.Dependency
 | 
			
		||||
import org.gradle.api.artifacts.MinimalExternalModuleDependency
 | 
			
		||||
import org.gradle.api.artifacts.ProjectDependency
 | 
			
		||||
import org.gradle.api.plugins.BasePluginExtension
 | 
			
		||||
import org.gradle.api.publish.maven.MavenPublication
 | 
			
		||||
import org.gradle.api.specs.Spec
 | 
			
		||||
 | 
			
		||||
@@ -26,8 +28,13 @@ class MavenDependencySpec {
 | 
			
		||||
 | 
			
		||||
    fun exclude(dep: Dependency) {
 | 
			
		||||
        exclude {
 | 
			
		||||
            // We have to cheat a little for project dependencies, as the project name doesn't match the artifact group.
 | 
			
		||||
            val name = when (dep) {
 | 
			
		||||
                is ProjectDependency -> dep.dependencyProject.extensions.getByType(BasePluginExtension::class.java).archivesName.get()
 | 
			
		||||
                else -> dep.name
 | 
			
		||||
            }
 | 
			
		||||
            (dep.group.isNullOrEmpty() || dep.group == it.groupId) &&
 | 
			
		||||
                (dep.name.isNullOrEmpty() || dep.name == it.artifactId) &&
 | 
			
		||||
                (name.isNullOrEmpty() || name == it.artifactId) &&
 | 
			
		||||
                (dep.version.isNullOrEmpty() || dep.version == it.version)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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,88 +82,39 @@ 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))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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))
 | 
			
		||||
        // Register a task to check there are no conflicts with the core project.
 | 
			
		||||
        val checkDependencyConsistency =
 | 
			
		||||
            project.tasks.register("checkDependencyConsistency", DependencyCheck::class.java) {
 | 
			
		||||
                // We need to check both the main and client classpath *configurations*, as the actual configuration
 | 
			
		||||
                configuration.add(configurations.named(main.runtimeClasspathConfigurationName))
 | 
			
		||||
                configuration.add(configurations.named(client.runtimeClasspathConfigurationName))
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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) } }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        project.tasks.named("check") { dependsOn(checkDependencyConsistency) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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 {#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.
 | 
			
		||||
@@ -31,6 +31,7 @@ as documentation for breaking changes and "gotchas" one should look out for betw
 | 
			
		||||
   - `load`/`loadstring` defaults to using the global environment (`_G`) rather than the current coroutine's
 | 
			
		||||
     environment.
 | 
			
		||||
   - Support for dumping functions (`string.dump`) and loading binary chunks has been removed.
 | 
			
		||||
   - `math.random` now uses Lua 5.4's random number generator.
 | 
			
		||||
 | 
			
		||||
 - File handles, HTTP requests and websockets now always use the original bytes rather than encoding/decoding to UTF-8.
 | 
			
		||||
 | 
			
		||||
@@ -75,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"
 | 
			
		||||
 
 | 
			
		||||
@@ -9,8 +9,8 @@ kotlin.stdlib.default.dependency=false
 | 
			
		||||
kotlin.jvm.target.validation.mode=error
 | 
			
		||||
 | 
			
		||||
# Mod properties
 | 
			
		||||
isUnstable=true
 | 
			
		||||
modVersion=1.109.0
 | 
			
		||||
isUnstable=false
 | 
			
		||||
modVersion=1.109.7
 | 
			
		||||
 | 
			
		||||
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
 | 
			
		||||
mcVersion=1.20.1
 | 
			
		||||
 
 | 
			
		||||
@@ -10,28 +10,30 @@
 | 
			
		||||
fabric-api = "0.86.1+1.20.1"
 | 
			
		||||
fabric-loader = "0.14.21"
 | 
			
		||||
forge = "47.1.0"
 | 
			
		||||
forgeSpi = "6.0.0"
 | 
			
		||||
forgeSpi = "7.0.1"
 | 
			
		||||
mixin = "0.8.5"
 | 
			
		||||
parchment = "2023.08.20"
 | 
			
		||||
parchmentMc = "1.20.1"
 | 
			
		||||
yarn = "1.20.1+build.10"
 | 
			
		||||
 | 
			
		||||
# Normal dependencies
 | 
			
		||||
asm = "9.5"
 | 
			
		||||
autoService = "1.1.1"
 | 
			
		||||
checkerFramework = "3.32.0"
 | 
			
		||||
cobalt = "0.8.0"
 | 
			
		||||
cobalt-next = "0.8.1" # Not a real version, used to constrain the version we accept.
 | 
			
		||||
commonsCli = "1.3.1"
 | 
			
		||||
# Core dependencies (these versions are tied to the version Minecraft uses)
 | 
			
		||||
fastutil = "8.5.9"
 | 
			
		||||
guava = "31.1-jre"
 | 
			
		||||
jetbrainsAnnotations = "24.0.1"
 | 
			
		||||
netty = "4.1.82.Final"
 | 
			
		||||
slf4j = "2.0.1"
 | 
			
		||||
 | 
			
		||||
# Core dependencies (independent of Minecraft)
 | 
			
		||||
asm = "9.6"
 | 
			
		||||
autoService = "1.1.1"
 | 
			
		||||
checkerFramework = "3.42.0"
 | 
			
		||||
cobalt = "0.9.1"
 | 
			
		||||
commonsCli = "1.6.0"
 | 
			
		||||
jetbrainsAnnotations = "24.1.0"
 | 
			
		||||
jsr305 = "3.0.2"
 | 
			
		||||
jzlib = "1.1.3"
 | 
			
		||||
kotlin = "1.8.10"
 | 
			
		||||
kotlin-coroutines = "1.6.4"
 | 
			
		||||
netty = "4.1.82.Final"
 | 
			
		||||
kotlin = "1.9.21"
 | 
			
		||||
kotlin-coroutines = "1.7.3"
 | 
			
		||||
nightConfig = "3.6.7"
 | 
			
		||||
slf4j = "2.0.1"
 | 
			
		||||
 | 
			
		||||
# Minecraft mods
 | 
			
		||||
emi = "1.0.8+1.20.1"
 | 
			
		||||
@@ -47,30 +49,32 @@ sodium = "mc1.20-0.4.10"
 | 
			
		||||
 | 
			
		||||
# Testing
 | 
			
		||||
hamcrest = "2.2"
 | 
			
		||||
jqwik = "1.7.4"
 | 
			
		||||
junit = "5.10.0"
 | 
			
		||||
jqwik = "1.8.2"
 | 
			
		||||
junit = "5.10.1"
 | 
			
		||||
jmh = "1.37"
 | 
			
		||||
 | 
			
		||||
# Build tools
 | 
			
		||||
cctJavadoc = "1.8.1"
 | 
			
		||||
checkstyle = "10.12.3"
 | 
			
		||||
cctJavadoc = "1.8.2"
 | 
			
		||||
checkstyle = "10.12.6"
 | 
			
		||||
curseForgeGradle = "1.0.14"
 | 
			
		||||
errorProne-core = "2.21.1"
 | 
			
		||||
errorProne-core = "2.23.0"
 | 
			
		||||
errorProne-plugin = "3.1.0"
 | 
			
		||||
fabric-loom = "1.3.7"
 | 
			
		||||
forgeGradle = "6.0.8"
 | 
			
		||||
githubRelease = "2.4.1"
 | 
			
		||||
fabric-loom = "1.5.7"
 | 
			
		||||
forgeGradle = "6.0.20"
 | 
			
		||||
githubRelease = "2.5.2"
 | 
			
		||||
gradleVersions = "0.50.0"
 | 
			
		||||
ideaExt = "1.1.7"
 | 
			
		||||
illuaminate = "0.1.0-44-g9ee0055"
 | 
			
		||||
librarian = "1.+"
 | 
			
		||||
lwjgl = "3.3.1"
 | 
			
		||||
lwjgl = "3.3.3"
 | 
			
		||||
minotaur = "2.+"
 | 
			
		||||
mixinGradle = "0.7.+"
 | 
			
		||||
mixinGradle = "0.7.38"
 | 
			
		||||
nullAway = "0.9.9"
 | 
			
		||||
spotless = "6.21.0"
 | 
			
		||||
spotless = "6.23.3"
 | 
			
		||||
taskTree = "2.1.1"
 | 
			
		||||
teavm = "0.10.0-SQUID.1"
 | 
			
		||||
vanillaGradle = "0.2.1-SNAPSHOT"
 | 
			
		||||
vineflower = "1.11.0"
 | 
			
		||||
teavm = "0.10.0-SQUID.2"
 | 
			
		||||
vanillaExtract = "0.1.1"
 | 
			
		||||
versionCatalogUpdate = "0.8.1"
 | 
			
		||||
 | 
			
		||||
[libraries]
 | 
			
		||||
# Normal dependencies
 | 
			
		||||
@@ -124,6 +128,8 @@ junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.re
 | 
			
		||||
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }
 | 
			
		||||
junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" }
 | 
			
		||||
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
 | 
			
		||||
jmh = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" }
 | 
			
		||||
jmh-processor = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" }
 | 
			
		||||
 | 
			
		||||
# LWJGL
 | 
			
		||||
lwjgl-bom = { module = "org.lwjgl:lwjgl-bom", version.ref = "lwjgl" }
 | 
			
		||||
@@ -156,16 +162,18 @@ 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" }
 | 
			
		||||
githubRelease = { id = "com.github.breadmoirai.github-release", version.ref = "githubRelease" }
 | 
			
		||||
gradleVersions = { id = "com.github.ben-manes.versions", version.ref = "gradleVersions" }
 | 
			
		||||
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
 | 
			
		||||
librarian = { id = "org.parchmentmc.librarian.forgegradle", version.ref = "librarian" }
 | 
			
		||||
mixinGradle = { id = "org.spongepowered.mixin", version.ref = "mixinGradle" }
 | 
			
		||||
taskTree = { id = "com.dorongold.task-tree", version.ref = "taskTree" }
 | 
			
		||||
versionCatalogUpdate = { id = "nl.littlerobots.version-catalog-update", version.ref = "versionCatalogUpdate" }
 | 
			
		||||
 | 
			
		||||
[bundles]
 | 
			
		||||
annotations = ["jsr305", "checkerFramework", "jetbrainsAnnotations"]
 | 
			
		||||
@@ -175,7 +183,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"]
 | 
			
		||||
 | 
			
		||||
@@ -184,5 +191,5 @@ test = ["junit-jupiter-api", "junit-jupiter-params", "hamcrest", "jqwik-api"]
 | 
			
		||||
testRuntime = ["junit-jupiter-engine", "jqwik-engine"]
 | 
			
		||||
 | 
			
		||||
# Build tools
 | 
			
		||||
teavm-api = [ "teavm-jso", "teavm-jso-apis", "teavm-platform", "teavm-classlib", "teavm-metaprogramming-api" ]
 | 
			
		||||
teavm-tooling = [ "teavm-tooling", "teavm-metaprogramming-impl", "teavm-jso-impl" ]
 | 
			
		||||
teavm-api = ["teavm-jso", "teavm-jso-apis", "teavm-platform", "teavm-classlib", "teavm-metaprogramming-api"]
 | 
			
		||||
teavm-tooling = ["teavm-tooling", "teavm-metaprogramming-impl", "teavm-jso-impl"]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											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" \
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@
 | 
			
		||||
 | 
			
		||||
(sources
 | 
			
		||||
  /doc/
 | 
			
		||||
  /projects/forge/build/docs/luaJavadoc/
 | 
			
		||||
  /projects/common/build/docs/luaJavadoc/
 | 
			
		||||
  /projects/core/src/main/resources/data/computercraft/lua/bios.lua
 | 
			
		||||
  /projects/core/src/main/resources/data/computercraft/lua/rom/
 | 
			
		||||
  /projects/core/src/test/resources/test-rom
 | 
			
		||||
@@ -36,7 +36,7 @@
 | 
			
		||||
 | 
			
		||||
  (library-path
 | 
			
		||||
    /doc/stub/
 | 
			
		||||
    /projects/forge/build/docs/luaJavadoc/
 | 
			
		||||
    /projects/common/build/docs/luaJavadoc/
 | 
			
		||||
 | 
			
		||||
    /projects/core/src/main/resources/data/computercraft/lua/rom/apis/
 | 
			
		||||
    /projects/core/src/main/resources/data/computercraft/lua/rom/apis/command/
 | 
			
		||||
@@ -88,7 +88,7 @@
 | 
			
		||||
  (/doc/stub/
 | 
			
		||||
   /projects/core/src/main/resources/data/computercraft/lua/bios.lua
 | 
			
		||||
   /projects/core/src/main/resources/data/computercraft/lua/rom/apis/
 | 
			
		||||
   /projects/forge/build/docs/luaJavadoc/)
 | 
			
		||||
   /projects/common/build/docs/luaJavadoc/)
 | 
			
		||||
  (linters -var:unused-global)
 | 
			
		||||
  (lint (allow-toplevel-global true)))
 | 
			
		||||
 | 
			
		||||
@@ -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> {
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
package dan200.computercraft.api.network.wired;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import org.jetbrains.annotations.ApiStatus;
 | 
			
		||||
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
@@ -22,6 +23,7 @@ import java.util.Map;
 | 
			
		||||
 *
 | 
			
		||||
 * @see WiredNode#getNetwork()
 | 
			
		||||
 */
 | 
			
		||||
@ApiStatus.NonExtendable
 | 
			
		||||
public interface WiredNetwork {
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a connection between two nodes.
 | 
			
		||||
@@ -35,7 +37,9 @@ public interface WiredNetwork {
 | 
			
		||||
     * @throws IllegalArgumentException If {@code left} and {@code right} are equal.
 | 
			
		||||
     * @see WiredNode#connectTo(WiredNode)
 | 
			
		||||
     * @see WiredNetwork#connect(WiredNode, WiredNode)
 | 
			
		||||
     * @deprecated Use {@link WiredNode#connectTo(WiredNode)}
 | 
			
		||||
     */
 | 
			
		||||
    @Deprecated
 | 
			
		||||
    boolean connect(WiredNode left, WiredNode right);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -50,7 +54,9 @@ public interface WiredNetwork {
 | 
			
		||||
     * @throws IllegalArgumentException If {@code left} and {@code right} are equal.
 | 
			
		||||
     * @see WiredNode#disconnectFrom(WiredNode)
 | 
			
		||||
     * @see WiredNetwork#connect(WiredNode, WiredNode)
 | 
			
		||||
     * @deprecated Use {@link WiredNode#disconnectFrom(WiredNode)}
 | 
			
		||||
     */
 | 
			
		||||
    @Deprecated
 | 
			
		||||
    boolean disconnect(WiredNode left, WiredNode right);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -64,7 +70,9 @@ public interface WiredNetwork {
 | 
			
		||||
     * only element.
 | 
			
		||||
     * @throws IllegalArgumentException If the node is not in the network.
 | 
			
		||||
     * @see WiredNode#remove()
 | 
			
		||||
     * @deprecated Use {@link WiredNode#remove()}
 | 
			
		||||
     */
 | 
			
		||||
    @Deprecated
 | 
			
		||||
    boolean remove(WiredNode node);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -77,6 +85,8 @@ public interface WiredNetwork {
 | 
			
		||||
     * @param peripherals The new peripherals for this node.
 | 
			
		||||
     * @throws IllegalArgumentException If the node is not in the network.
 | 
			
		||||
     * @see WiredNode#updatePeripherals(Map)
 | 
			
		||||
     * @deprecated Use {@link WiredNode#updatePeripherals(Map)}
 | 
			
		||||
     */
 | 
			
		||||
    @Deprecated
 | 
			
		||||
    void updatePeripherals(WiredNode node, Map<String, IPeripheral> peripherals);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ package dan200.computercraft.api.network.wired;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.network.PacketNetwork;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import org.jetbrains.annotations.ApiStatus;
 | 
			
		||||
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
@@ -22,6 +23,7 @@ import java.util.Map;
 | 
			
		||||
 * Wired nodes also provide several convenience methods for interacting with a wired network. These should only ever
 | 
			
		||||
 * be used on the main server thread.
 | 
			
		||||
 */
 | 
			
		||||
@ApiStatus.NonExtendable
 | 
			
		||||
public interface WiredNode extends PacketNetwork {
 | 
			
		||||
    /**
 | 
			
		||||
     * The associated element for this network node.
 | 
			
		||||
@@ -37,7 +39,9 @@ public interface WiredNode extends PacketNetwork {
 | 
			
		||||
     * This should only be used on the server thread.
 | 
			
		||||
     *
 | 
			
		||||
     * @return This node's network.
 | 
			
		||||
     * @deprecated Use the connect/disconnect/remove methods on {@link WiredNode}.
 | 
			
		||||
     */
 | 
			
		||||
    @Deprecated
 | 
			
		||||
    WiredNetwork getNetwork();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -47,12 +51,9 @@ public interface WiredNode extends PacketNetwork {
 | 
			
		||||
     *
 | 
			
		||||
     * @param node The other node to connect to.
 | 
			
		||||
     * @return {@code true} if a connection was created or {@code false} if the connection already exists.
 | 
			
		||||
     * @see WiredNetwork#connect(WiredNode, WiredNode)
 | 
			
		||||
     * @see WiredNode#disconnectFrom(WiredNode)
 | 
			
		||||
     */
 | 
			
		||||
    default boolean connectTo(WiredNode node) {
 | 
			
		||||
        return getNetwork().connect(this, node);
 | 
			
		||||
    }
 | 
			
		||||
    boolean connectTo(WiredNode node);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Destroy a connection between this node and another.
 | 
			
		||||
@@ -61,13 +62,9 @@ public interface WiredNode extends PacketNetwork {
 | 
			
		||||
     *
 | 
			
		||||
     * @param node The other node to disconnect from.
 | 
			
		||||
     * @return {@code true} if a connection was destroyed or {@code false} if no connection exists.
 | 
			
		||||
     * @throws IllegalArgumentException If {@code node} is not on the same network.
 | 
			
		||||
     * @see WiredNetwork#disconnect(WiredNode, WiredNode)
 | 
			
		||||
     * @see WiredNode#connectTo(WiredNode)
 | 
			
		||||
     */
 | 
			
		||||
    default boolean disconnectFrom(WiredNode node) {
 | 
			
		||||
        return getNetwork().disconnect(this, node);
 | 
			
		||||
    }
 | 
			
		||||
    boolean disconnectFrom(WiredNode node);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sever all connections this node has, removing it from this network.
 | 
			
		||||
@@ -78,11 +75,8 @@ public interface WiredNode extends PacketNetwork {
 | 
			
		||||
     * @return Whether this node was removed from the network. One cannot remove a node from a network where it is the
 | 
			
		||||
     * only element.
 | 
			
		||||
     * @throws IllegalArgumentException If the node is not in the network.
 | 
			
		||||
     * @see WiredNetwork#remove(WiredNode)
 | 
			
		||||
     */
 | 
			
		||||
    default boolean remove() {
 | 
			
		||||
        return getNetwork().remove(this);
 | 
			
		||||
    }
 | 
			
		||||
    boolean remove();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Mark this node's peripherals as having changed.
 | 
			
		||||
@@ -91,9 +85,6 @@ public interface WiredNode extends PacketNetwork {
 | 
			
		||||
     * that your network element owns.
 | 
			
		||||
     *
 | 
			
		||||
     * @param peripherals The new peripherals for this node.
 | 
			
		||||
     * @see WiredNetwork#updatePeripherals(WiredNode, Map)
 | 
			
		||||
     */
 | 
			
		||||
    default void updatePeripherals(Map<String, IPeripheral> peripherals) {
 | 
			
		||||
        getNetwork().updatePeripherals(this, peripherals);
 | 
			
		||||
    }
 | 
			
		||||
    void updatePeripherals(Map<String, IPeripheral> peripherals);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
 *
 | 
			
		||||
 
 | 
			
		||||
@@ -2,13 +2,12 @@
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
import cc.tweaked.gradle.annotationProcessorEverywhere
 | 
			
		||||
import cc.tweaked.gradle.clientClasses
 | 
			
		||||
import cc.tweaked.gradle.commonClasses
 | 
			
		||||
import cc.tweaked.gradle.*
 | 
			
		||||
 | 
			
		||||
plugins {
 | 
			
		||||
    id("cc-tweaked.vanilla")
 | 
			
		||||
    id("cc-tweaked.gametest")
 | 
			
		||||
    id("cc-tweaked.illuaminate")
 | 
			
		||||
    id("cc-tweaked.publishing")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -19,6 +18,18 @@ minecraft {
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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"))
 | 
			
		||||
@@ -28,7 +39,6 @@ dependencies {
 | 
			
		||||
    compileOnly(libs.bundles.externalMods.common)
 | 
			
		||||
    clientCompileOnly(variantOf(libs.emi) { classifier("api") })
 | 
			
		||||
 | 
			
		||||
    compileOnly(libs.mixin)
 | 
			
		||||
    annotationProcessorEverywhere(libs.autoService)
 | 
			
		||||
    testFixturesAnnotationProcessor(libs.autoService)
 | 
			
		||||
 | 
			
		||||
@@ -36,9 +46,62 @@ dependencies {
 | 
			
		||||
    testImplementation(libs.bundles.test)
 | 
			
		||||
    testRuntimeOnly(libs.bundles.testRuntime)
 | 
			
		||||
 | 
			
		||||
    testImplementation(libs.jmh)
 | 
			
		||||
    testAnnotationProcessor(libs.jmh.processor)
 | 
			
		||||
 | 
			
		||||
    testModCompileOnly(libs.mixin)
 | 
			
		||||
    testModImplementation(testFixtures(project(":core")))
 | 
			
		||||
    testModImplementation(testFixtures(project(":common")))
 | 
			
		||||
    testModImplementation(libs.bundles.kotlin)
 | 
			
		||||
 | 
			
		||||
    testFixturesImplementation(testFixtures(project(":core")))
 | 
			
		||||
 | 
			
		||||
    "cctJavadoc"(libs.cctJavadoc)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
illuaminate {
 | 
			
		||||
    version.set(libs.versions.illuaminate)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
val luaJavadoc by tasks.registering(Javadoc::class) {
 | 
			
		||||
    description = "Generates documentation for Java-side Lua functions."
 | 
			
		||||
    group = JavaBasePlugin.DOCUMENTATION_GROUP
 | 
			
		||||
 | 
			
		||||
    val sourceSets = listOf(sourceSets.main.get(), project(":core").sourceSets.main.get())
 | 
			
		||||
    for (sourceSet in sourceSets) {
 | 
			
		||||
        source(sourceSet.java)
 | 
			
		||||
        classpath += sourceSet.compileClasspath
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    destinationDir = layout.buildDirectory.dir("docs/luaJavadoc").get().asFile
 | 
			
		||||
 | 
			
		||||
    val options = options as StandardJavadocDocletOptions
 | 
			
		||||
    options.docletpath = configurations["cctJavadoc"].files.toList()
 | 
			
		||||
    options.doclet = "cc.tweaked.javadoc.LuaDoclet"
 | 
			
		||||
    options.addStringOption("project-root", rootProject.file(".").absolutePath)
 | 
			
		||||
    options.noTimestamp(false)
 | 
			
		||||
 | 
			
		||||
    javadocTool.set(
 | 
			
		||||
        javaToolchains.javadocToolFor {
 | 
			
		||||
            languageVersion.set(CCTweakedPlugin.JAVA_VERSION)
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
val lintLua by tasks.registering(IlluaminateExec::class) {
 | 
			
		||||
    group = JavaBasePlugin.VERIFICATION_GROUP
 | 
			
		||||
    description = "Lint Lua (and Lua docs) with illuaminate"
 | 
			
		||||
 | 
			
		||||
    // Config files
 | 
			
		||||
    inputs.file(rootProject.file("illuaminate.sexp")).withPropertyName("illuaminate.sexp")
 | 
			
		||||
    // Sources
 | 
			
		||||
    inputs.files(rootProject.fileTree("doc")).withPropertyName("docs")
 | 
			
		||||
    inputs.files(project(":core").fileTree("src/main/resources/data/computercraft/lua")).withPropertyName("lua rom")
 | 
			
		||||
    inputs.files(luaJavadoc)
 | 
			
		||||
 | 
			
		||||
    args = listOf("lint")
 | 
			
		||||
    workingDir = rootProject.projectDir
 | 
			
		||||
 | 
			
		||||
    doFirst { if (System.getenv("GITHUB_ACTIONS") != null) println("::add-matcher::.github/matchers/illuaminate.json") }
 | 
			
		||||
    doLast { if (System.getenv("GITHUB_ACTIONS") != null) println("::remove-matcher owner=illuaminate::") }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -19,6 +19,7 @@ import dan200.computercraft.shared.network.server.UploadFileMessage;
 | 
			
		||||
import net.minecraft.ChatFormatting;
 | 
			
		||||
import net.minecraft.Util;
 | 
			
		||||
import net.minecraft.client.gui.GuiGraphics;
 | 
			
		||||
import net.minecraft.client.gui.components.events.GuiEventListener;
 | 
			
		||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.world.entity.player.Inventory;
 | 
			
		||||
@@ -144,6 +145,11 @@ public abstract class AbstractComputerScreen<T extends AbstractComputerMenu> ext
 | 
			
		||||
            || super.mouseDragged(x, y, button, deltaX, deltaY);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setFocused(@Nullable GuiEventListener listener) {
 | 
			
		||||
        // Don't clear and re-focus if we're already focused.
 | 
			
		||||
        if (listener != getFocused()) super.setFocused(listener);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) {
 | 
			
		||||
@@ -201,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));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -246,7 +246,7 @@ public class TerminalWidget extends AbstractWidget {
 | 
			
		||||
            keysDown.clear();
 | 
			
		||||
 | 
			
		||||
            // When blurring, we should make the last mouse button go up
 | 
			
		||||
            if (lastMouseButton > 0) {
 | 
			
		||||
            if (lastMouseButton >= 0) {
 | 
			
		||||
                computer.mouseUp(lastMouseButton + 1, lastMouseX + 1, lastMouseY + 1);
 | 
			
		||||
                lastMouseButton = -1;
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,6 @@ import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.api.turtle.TurtleSide;
 | 
			
		||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
 | 
			
		||||
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
 | 
			
		||||
import dan200.computercraft.shared.util.Holiday;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
@@ -21,7 +20,6 @@ import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher;
 | 
			
		||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
 | 
			
		||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
 | 
			
		||||
import net.minecraft.client.resources.model.BakedModel;
 | 
			
		||||
import net.minecraft.client.resources.model.ModelResourceLocation;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.phys.BlockHitResult;
 | 
			
		||||
import net.minecraft.world.phys.HitResult;
 | 
			
		||||
@@ -29,8 +27,6 @@ import net.minecraft.world.phys.HitResult;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBlockEntity> {
 | 
			
		||||
    private static final ModelResourceLocation NORMAL_TURTLE_MODEL = new ModelResourceLocation(ComputerCraftAPI.MOD_ID, "turtle_normal", "inventory");
 | 
			
		||||
    private static final ModelResourceLocation ADVANCED_TURTLE_MODEL = new ModelResourceLocation(ComputerCraftAPI.MOD_ID, "turtle_advanced", "inventory");
 | 
			
		||||
    private static final ResourceLocation COLOUR_TURTLE_MODEL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_colour");
 | 
			
		||||
    private static final ResourceLocation ELF_OVERLAY_MODEL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_elf_overlay");
 | 
			
		||||
 | 
			
		||||
@@ -42,13 +38,6 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
 | 
			
		||||
        font = context.getFont();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static ResourceLocation getTurtleModel(ComputerFamily family, boolean coloured) {
 | 
			
		||||
        return switch (family) {
 | 
			
		||||
            default -> coloured ? COLOUR_TURTLE_MODEL : NORMAL_TURTLE_MODEL;
 | 
			
		||||
            case ADVANCED -> coloured ? COLOUR_TURTLE_MODEL : ADVANCED_TURTLE_MODEL;
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static @Nullable ResourceLocation getTurtleOverlayModel(@Nullable ResourceLocation overlay, boolean christmas) {
 | 
			
		||||
        if (overlay != null) return overlay;
 | 
			
		||||
        if (christmas) return ELF_OVERLAY_MODEL;
 | 
			
		||||
@@ -78,7 +67,6 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
 | 
			
		||||
            var matrix = transform.last().pose();
 | 
			
		||||
            var opacity = (int) (mc.options.getBackgroundOpacity(0.25f) * 255) << 24;
 | 
			
		||||
            var width = -font.width(label) / 2.0f;
 | 
			
		||||
            // TODO: Check this looks okay
 | 
			
		||||
            font.drawInBatch(label, width, (float) 0, 0x20ffffff, false, matrix, buffers, Font.DisplayMode.SEE_THROUGH, opacity, lightmapCoord);
 | 
			
		||||
            font.drawInBatch(label, width, (float) 0, 0xffffffff, false, matrix, buffers, Font.DisplayMode.NORMAL, 0, lightmapCoord);
 | 
			
		||||
 | 
			
		||||
@@ -96,10 +84,18 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
 | 
			
		||||
 | 
			
		||||
        // Render the turtle
 | 
			
		||||
        var colour = turtle.getColour();
 | 
			
		||||
        var family = turtle.getFamily();
 | 
			
		||||
        var overlay = turtle.getOverlay();
 | 
			
		||||
 | 
			
		||||
        renderModel(transform, buffers, lightmapCoord, overlayLight, getTurtleModel(family, colour != -1), colour == -1 ? null : new int[]{ colour });
 | 
			
		||||
        if (colour == -1) {
 | 
			
		||||
            // Render the turtle using its item model.
 | 
			
		||||
            var modelManager = Minecraft.getInstance().getItemRenderer().getItemModelShaper();
 | 
			
		||||
            var model = modelManager.getItemModel(turtle.getBlockState().getBlock().asItem());
 | 
			
		||||
            if (model == null) model = modelManager.getModelManager().getMissingModel();
 | 
			
		||||
            renderModel(transform, buffers, lightmapCoord, overlayLight, model, null);
 | 
			
		||||
        } else {
 | 
			
		||||
            // Otherwise render it using the colour item.
 | 
			
		||||
            renderModel(transform, buffers, lightmapCoord, overlayLight, COLOUR_TURTLE_MODEL, new int[]{ colour });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Render the overlay
 | 
			
		||||
        var overlayModel = getTurtleOverlayModel(overlay, Holiday.getCurrent() == Holiday.CHRISTMAS);
 | 
			
		||||
 
 | 
			
		||||
@@ -58,9 +58,9 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
 | 
			
		||||
    @Override
 | 
			
		||||
    public void render(MonitorBlockEntity monitor, float partialTicks, PoseStack transform, MultiBufferSource bufferSource, int lightmapCoord, int overlayLight) {
 | 
			
		||||
        // Render from the origin monitor
 | 
			
		||||
        var originTerminal = monitor.getClientMonitor();
 | 
			
		||||
 | 
			
		||||
        var originTerminal = monitor.getOriginClientMonitor();
 | 
			
		||||
        if (originTerminal == null) return;
 | 
			
		||||
 | 
			
		||||
        var origin = originTerminal.getOrigin();
 | 
			
		||||
        var renderState = originTerminal.getRenderState(MonitorRenderState::new);
 | 
			
		||||
        var monitorPos = monitor.getBlockPos();
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,6 @@ import dan200.computercraft.api.upgrades.UpgradeData;
 | 
			
		||||
import dan200.computercraft.core.util.Colour;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import dan200.computercraft.shared.common.IColouredItem;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import dan200.computercraft.shared.platform.PlatformHelper;
 | 
			
		||||
import dan200.computercraft.shared.platform.RecipeIngredients;
 | 
			
		||||
import dan200.computercraft.shared.platform.RegistryWrappers;
 | 
			
		||||
@@ -38,7 +37,6 @@ import net.minecraft.world.level.ItemLike;
 | 
			
		||||
import net.minecraft.world.level.block.Blocks;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Locale;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
 | 
			
		||||
@@ -106,14 +104,13 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
 | 
			
		||||
    private void turtleUpgrades(Consumer<FinishedRecipe> add) {
 | 
			
		||||
        for (var turtleItem : turtleItems()) {
 | 
			
		||||
            var base = turtleItem.create(-1, null, -1, null, null, 0, null);
 | 
			
		||||
 | 
			
		||||
            var nameId = turtleItem.getFamily().name().toLowerCase(Locale.ROOT);
 | 
			
		||||
            var name = RegistryWrappers.ITEMS.getKey(turtleItem);
 | 
			
		||||
 | 
			
		||||
            for (var upgrade : turtleUpgrades.getGeneratedUpgrades()) {
 | 
			
		||||
                var result = turtleItem.create(-1, null, -1, null, UpgradeData.ofDefault(upgrade), -1, null);
 | 
			
		||||
                ShapedRecipeBuilder
 | 
			
		||||
                    .shaped(RecipeCategory.REDSTONE, result.getItem())
 | 
			
		||||
                    .group(String.format("%s:turtle_%s", ComputerCraftAPI.MOD_ID, nameId))
 | 
			
		||||
                    .group(name.toString())
 | 
			
		||||
                    .pattern("#T")
 | 
			
		||||
                    .define('T', base.getItem())
 | 
			
		||||
                    .define('#', upgrade.getCraftingItem().getItem())
 | 
			
		||||
@@ -121,9 +118,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
 | 
			
		||||
                        inventoryChange(base.getItem(), upgrade.getCraftingItem().getItem()))
 | 
			
		||||
                    .save(
 | 
			
		||||
                        RecipeWrapper.wrap(ModRegistry.RecipeSerializers.IMPOSTOR_SHAPED.get(), add).withResultTag(result.getTag()),
 | 
			
		||||
                        new ResourceLocation(ComputerCraftAPI.MOD_ID, String.format("turtle_%s/%s/%s",
 | 
			
		||||
                            nameId, upgrade.getUpgradeID().getNamespace(), upgrade.getUpgradeID().getPath()
 | 
			
		||||
                        ))
 | 
			
		||||
                        name.withSuffix(String.format("/%s/%s", upgrade.getUpgradeID().getNamespace(), upgrade.getUpgradeID().getPath()))
 | 
			
		||||
                    );
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -141,15 +136,13 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
 | 
			
		||||
    private void pocketUpgrades(Consumer<FinishedRecipe> add) {
 | 
			
		||||
        for (var pocket : pocketComputerItems()) {
 | 
			
		||||
            var base = pocket.create(-1, null, -1, null);
 | 
			
		||||
            if (base.isEmpty()) continue;
 | 
			
		||||
 | 
			
		||||
            var nameId = pocket.getFamily().name().toLowerCase(Locale.ROOT);
 | 
			
		||||
            var name = RegistryWrappers.ITEMS.getKey(pocket).withPath(x -> x.replace("pocket_computer_", "pocket_"));
 | 
			
		||||
 | 
			
		||||
            for (var upgrade : pocketUpgrades.getGeneratedUpgrades()) {
 | 
			
		||||
                var result = pocket.create(-1, null, -1, UpgradeData.ofDefault(upgrade));
 | 
			
		||||
                ShapedRecipeBuilder
 | 
			
		||||
                    .shaped(RecipeCategory.REDSTONE, result.getItem())
 | 
			
		||||
                    .group(String.format("%s:pocket_%s", ComputerCraftAPI.MOD_ID, nameId))
 | 
			
		||||
                    .group(name.toString())
 | 
			
		||||
                    .pattern("#")
 | 
			
		||||
                    .pattern("P")
 | 
			
		||||
                    .define('P', base.getItem())
 | 
			
		||||
@@ -158,9 +151,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
 | 
			
		||||
                        inventoryChange(base.getItem(), upgrade.getCraftingItem().getItem()))
 | 
			
		||||
                    .save(
 | 
			
		||||
                        RecipeWrapper.wrap(ModRegistry.RecipeSerializers.IMPOSTOR_SHAPED.get(), add).withResultTag(result.getTag()),
 | 
			
		||||
                        new ResourceLocation(ComputerCraftAPI.MOD_ID, String.format("pocket_%s/%s/%s",
 | 
			
		||||
                            nameId, upgrade.getUpgradeID().getNamespace(), upgrade.getUpgradeID().getPath()
 | 
			
		||||
                        ))
 | 
			
		||||
                        name.withSuffix(String.format("/%s/%s", upgrade.getUpgradeID().getNamespace(), upgrade.getUpgradeID().getPath()))
 | 
			
		||||
                    );
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -190,12 +181,10 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
 | 
			
		||||
    private void turtleOverlay(Consumer<FinishedRecipe> add, String overlay, Consumer<ShapelessRecipeBuilder> build) {
 | 
			
		||||
        for (var turtleItem : turtleItems()) {
 | 
			
		||||
            var base = turtleItem.create(-1, null, -1, null, null, 0, null);
 | 
			
		||||
 | 
			
		||||
            var nameId = turtleItem.getFamily().name().toLowerCase(Locale.ROOT);
 | 
			
		||||
            var group = "%s:turtle_%s_overlay".formatted(ComputerCraftAPI.MOD_ID, nameId);
 | 
			
		||||
            var name = RegistryWrappers.ITEMS.getKey(turtleItem);
 | 
			
		||||
 | 
			
		||||
            var builder = ShapelessRecipeBuilder.shapeless(RecipeCategory.REDSTONE, base.getItem())
 | 
			
		||||
                .group(group)
 | 
			
		||||
                .group(name.withSuffix("_overlay").toString())
 | 
			
		||||
                .unlockedBy("has_turtle", inventoryChange(base.getItem()));
 | 
			
		||||
            build.accept(builder);
 | 
			
		||||
            builder
 | 
			
		||||
@@ -204,7 +193,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
 | 
			
		||||
                    RecipeWrapper
 | 
			
		||||
                        .wrap(ModRegistry.RecipeSerializers.TURTLE_OVERLAY.get(), add)
 | 
			
		||||
                        .withExtraData(x -> x.addProperty("overlay", new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/" + overlay).toString())),
 | 
			
		||||
                    new ResourceLocation(ComputerCraftAPI.MOD_ID, "turtle_%s_overlays/%s".formatted(nameId, overlay))
 | 
			
		||||
                    name.withSuffix("_overlays/" + overlay)
 | 
			
		||||
                );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -253,7 +242,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
 | 
			
		||||
            .define('C', ModRegistry.Items.COMPUTER_NORMAL.get())
 | 
			
		||||
            .unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.COMPUTER_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
 | 
			
		||||
            .save(
 | 
			
		||||
                RecipeWrapper.wrap(ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get(), add).withExtraData(family(ComputerFamily.ADVANCED)),
 | 
			
		||||
                RecipeWrapper.wrap(ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get(), add),
 | 
			
		||||
                new ResourceLocation(ComputerCraftAPI.MOD_ID, "computer_advanced_upgrade")
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
@@ -277,7 +266,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
 | 
			
		||||
            .define('C', ModRegistry.Items.COMPUTER_NORMAL.get())
 | 
			
		||||
            .define('I', ingredients.woodenChest())
 | 
			
		||||
            .unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_NORMAL.get()))
 | 
			
		||||
            .save(RecipeWrapper.wrap(ModRegistry.RecipeSerializers.TURTLE.get(), add).withExtraData(family(ComputerFamily.NORMAL)));
 | 
			
		||||
            .save(RecipeWrapper.wrap(ModRegistry.RecipeSerializers.TURTLE.get(), add));
 | 
			
		||||
 | 
			
		||||
        ShapedRecipeBuilder
 | 
			
		||||
            .shaped(RecipeCategory.REDSTONE, ModRegistry.Blocks.TURTLE_ADVANCED.get())
 | 
			
		||||
@@ -288,7 +277,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
 | 
			
		||||
            .define('C', ModRegistry.Items.COMPUTER_ADVANCED.get())
 | 
			
		||||
            .define('I', ingredients.woodenChest())
 | 
			
		||||
            .unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_NORMAL.get()))
 | 
			
		||||
            .save(RecipeWrapper.wrap(ModRegistry.RecipeSerializers.TURTLE.get(), add).withExtraData(family(ComputerFamily.ADVANCED)));
 | 
			
		||||
            .save(RecipeWrapper.wrap(ModRegistry.RecipeSerializers.TURTLE.get(), add));
 | 
			
		||||
 | 
			
		||||
        ShapedRecipeBuilder
 | 
			
		||||
            .shaped(RecipeCategory.REDSTONE, ModRegistry.Blocks.TURTLE_ADVANCED.get())
 | 
			
		||||
@@ -300,7 +289,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
 | 
			
		||||
            .define('B', ingredients.goldBlock())
 | 
			
		||||
            .unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.TURTLE_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
 | 
			
		||||
            .save(
 | 
			
		||||
                RecipeWrapper.wrap(ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get(), add).withExtraData(family(ComputerFamily.ADVANCED)),
 | 
			
		||||
                RecipeWrapper.wrap(ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get(), add),
 | 
			
		||||
                new ResourceLocation(ComputerCraftAPI.MOD_ID, "turtle_advanced_upgrade")
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
@@ -367,7 +356,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
 | 
			
		||||
            .define('C', ModRegistry.Items.POCKET_COMPUTER_NORMAL.get())
 | 
			
		||||
            .unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
 | 
			
		||||
            .save(
 | 
			
		||||
                RecipeWrapper.wrap(ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get(), add).withExtraData(family(ComputerFamily.ADVANCED)),
 | 
			
		||||
                RecipeWrapper.wrap(ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get(), add),
 | 
			
		||||
                new ResourceLocation(ComputerCraftAPI.MOD_ID, "pocket_computer_advanced_upgrade")
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
@@ -519,10 +508,6 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
 | 
			
		||||
        return tag;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static Consumer<JsonObject> family(ComputerFamily family) {
 | 
			
		||||
        return json -> json.addProperty("family", family.getSerializedName());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void addSpecial(Consumer<FinishedRecipe> add, SimpleCraftingRecipeSerializer<?> special) {
 | 
			
		||||
        SpecialRecipeBuilder.special(special).save(add, RegistryWrappers.RECIPE_SERIALIZERS.getKey(special).toString());
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -58,7 +58,10 @@ class TagProvider {
 | 
			
		||||
 | 
			
		||||
        tags.tag(ComputerCraftTags.Blocks.TURTLE_SWORD_BREAKABLE).addTag(BlockTags.WOOL).add(Blocks.COBWEB);
 | 
			
		||||
 | 
			
		||||
        tags.tag(ComputerCraftTags.Blocks.TURTLE_CAN_USE).addTag(BlockTags.CAULDRONS).addTag(BlockTags.BEEHIVES);
 | 
			
		||||
        tags.tag(ComputerCraftTags.Blocks.TURTLE_CAN_USE)
 | 
			
		||||
            .addTag(BlockTags.BEEHIVES)
 | 
			
		||||
            .addTag(BlockTags.CAULDRONS)
 | 
			
		||||
            .add(Blocks.COMPOSTER);
 | 
			
		||||
 | 
			
		||||
        // Make all blocks aside from command computer mineable.
 | 
			
		||||
        tags.tag(BlockTags.MINEABLE_WITH_PICKAXE).add(
 | 
			
		||||
@@ -76,6 +79,8 @@ class TagProvider {
 | 
			
		||||
            ModRegistry.Blocks.WIRED_MODEM_FULL.get(),
 | 
			
		||||
            ModRegistry.Blocks.CABLE.get()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        tags.tag(BlockTags.WITHER_IMMUNE).add(ModRegistry.Blocks.COMPUTER_COMMAND.get());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void itemTags(ItemTagConsumer tags) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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)) {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,45 +4,66 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.impl.network.wired;
 | 
			
		||||
 | 
			
		||||
import org.jetbrains.annotations.Contract;
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Verifies certain elements of a network are "well formed".
 | 
			
		||||
 * Verifies certain elements of a network are well-formed.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This adds substantial overhead to network modification, and so should only be enabled
 | 
			
		||||
 * in a development environment.
 | 
			
		||||
 * This adds substantial overhead to network modification, and so is only enabled when assertions are enabled.
 | 
			
		||||
 */
 | 
			
		||||
public final class InvariantChecker {
 | 
			
		||||
final class InvariantChecker {
 | 
			
		||||
    private static final Logger LOG = LoggerFactory.getLogger(InvariantChecker.class);
 | 
			
		||||
    private static final boolean ENABLED = false;
 | 
			
		||||
 | 
			
		||||
    private InvariantChecker() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void checkNode(WiredNodeImpl node) {
 | 
			
		||||
        if (!ENABLED) return;
 | 
			
		||||
    static void checkNode(WiredNodeImpl node) {
 | 
			
		||||
        assert checkNodeImpl(node) : "Node invariants failed. See logs.";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        var network = node.network;
 | 
			
		||||
        if (network == null) {
 | 
			
		||||
            LOG.error("Node's network is null", new Exception());
 | 
			
		||||
            return;
 | 
			
		||||
    private static boolean checkNodeImpl(WiredNodeImpl node) {
 | 
			
		||||
        var okay = true;
 | 
			
		||||
 | 
			
		||||
        if (node.currentSet != null) {
 | 
			
		||||
            okay = false;
 | 
			
		||||
            LOG.error("{}: currentSet was not cleared.", node);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (network.nodes == null || !network.nodes.contains(node)) {
 | 
			
		||||
            LOG.error("Node's network does not contain node", new Exception());
 | 
			
		||||
        var network = makeNullable(node.network);
 | 
			
		||||
        if (network == null) {
 | 
			
		||||
            okay = false;
 | 
			
		||||
            LOG.error("{}: Node's network is null.", node);
 | 
			
		||||
        } else if (makeNullable(network.nodes) == null || !network.nodes.contains(node)) {
 | 
			
		||||
            okay = false;
 | 
			
		||||
            LOG.error("{}: Node's network does not contain node.", node);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (var neighbour : node.neighbours) {
 | 
			
		||||
            if (!neighbour.neighbours.contains(node)) {
 | 
			
		||||
                LOG.error("Neighbour is missing node", new Exception());
 | 
			
		||||
                okay = false;
 | 
			
		||||
                LOG.error("{}: Neighbour {}'s neighbour set does not contain origianl node.", node, neighbour);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return okay;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void checkNetwork(WiredNetworkImpl network) {
 | 
			
		||||
        if (!ENABLED) return;
 | 
			
		||||
    static void checkNetwork(WiredNetworkImpl network) {
 | 
			
		||||
        assert checkNetworkImpl(network) : "Network invariants failed. See logs.";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        for (var node : network.nodes) checkNode(node);
 | 
			
		||||
    private static boolean checkNetworkImpl(WiredNetworkImpl network) {
 | 
			
		||||
        var okay = true;
 | 
			
		||||
        for (var node : network.nodes) okay &= checkNodeImpl(node);
 | 
			
		||||
        return okay;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Contract("")
 | 
			
		||||
    private static <T> @Nullable T makeNullable(T object) {
 | 
			
		||||
        return object;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,100 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.impl.network.wired;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.network.wired.WiredNode;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A disjoint-set/union-find of {@link WiredNodeImpl}s.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Rather than actually maintaining a list of included nodes, wired nodes store {@linkplain WiredNodeImpl#currentSet the
 | 
			
		||||
 * set they're part of}. This means that we can only have one disjoint-set at once, but that is not a problem in
 | 
			
		||||
 * practice.
 | 
			
		||||
 *
 | 
			
		||||
 * @see WiredNodeImpl#currentSet
 | 
			
		||||
 * @see WiredNetworkImpl#remove(WiredNode)
 | 
			
		||||
 * @see <a href="https://en.wikipedia.org/wiki/Disjoint-set_data_structure">Disjoint-set data structure</a>
 | 
			
		||||
 */
 | 
			
		||||
class NodeSet {
 | 
			
		||||
    private NodeSet parent = this;
 | 
			
		||||
    private int size = 1;
 | 
			
		||||
    private @Nullable WiredNetworkImpl network;
 | 
			
		||||
 | 
			
		||||
    private boolean isRoot() {
 | 
			
		||||
        return parent == this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Resolve this union, finding the root {@link NodeSet}.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The root union.
 | 
			
		||||
     */
 | 
			
		||||
    NodeSet find() {
 | 
			
		||||
        var self = this;
 | 
			
		||||
        while (!self.isRoot()) self = self.parent = self.parent.parent;
 | 
			
		||||
        return self;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the size of this node set.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The size of the set.
 | 
			
		||||
     */
 | 
			
		||||
    int size() {
 | 
			
		||||
        return find().size;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add a node to this {@link NodeSet}.
 | 
			
		||||
     *
 | 
			
		||||
     * @param node The node to add to the set.
 | 
			
		||||
     */
 | 
			
		||||
    void addNode(WiredNodeImpl node) {
 | 
			
		||||
        if (!isRoot()) throw new IllegalStateException("Cannot grow a non-root set.");
 | 
			
		||||
        if (node.currentSet != null) throw new IllegalArgumentException("Node is already in a set.");
 | 
			
		||||
 | 
			
		||||
        node.currentSet = this;
 | 
			
		||||
        size++;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Merge two nodes sets together.
 | 
			
		||||
     *
 | 
			
		||||
     * @param left  The first union.
 | 
			
		||||
     * @param right The second union.
 | 
			
		||||
     * @return The union which was subsumed.
 | 
			
		||||
     */
 | 
			
		||||
    public static NodeSet merge(NodeSet left, NodeSet right) {
 | 
			
		||||
        if (!left.isRoot() || !right.isRoot()) throw new IllegalArgumentException("Cannot union a non-root set.");
 | 
			
		||||
        if (left == right) throw new IllegalArgumentException("Cannot merge a node into itself.");
 | 
			
		||||
 | 
			
		||||
        return left.size >= right.size ? mergeInto(left, right) : mergeInto(right, left);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static NodeSet mergeInto(NodeSet root, NodeSet child) {
 | 
			
		||||
        assert root.size > child.size;
 | 
			
		||||
        child.parent = root;
 | 
			
		||||
        root.size += child.size;
 | 
			
		||||
        return child;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void setNetwork(WiredNetworkImpl network) {
 | 
			
		||||
        if (!isRoot()) throw new IllegalStateException("Set is not the root.");
 | 
			
		||||
        if (this.network != null) throw new IllegalStateException("Set already has a network.");
 | 
			
		||||
        this.network = network;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the associated network.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The associated network.
 | 
			
		||||
     */
 | 
			
		||||
    WiredNetworkImpl network() {
 | 
			
		||||
        return Objects.requireNonNull(find().network);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -8,6 +8,7 @@ import dan200.computercraft.api.network.Packet;
 | 
			
		||||
import dan200.computercraft.api.network.wired.WiredNetwork;
 | 
			
		||||
import dan200.computercraft.api.network.wired.WiredNode;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import dan200.computercraft.core.util.Nullability;
 | 
			
		||||
 | 
			
		||||
import java.util.*;
 | 
			
		||||
import java.util.concurrent.locks.ReadWriteLock;
 | 
			
		||||
@@ -187,10 +188,76 @@ final class WiredNetworkImpl implements WiredNetwork {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var reachable = reachableNodes(neighbours.iterator().next());
 | 
			
		||||
            assert neighbours.size() >= 2 : "Must have more than one neighbour.";
 | 
			
		||||
 | 
			
		||||
            /*
 | 
			
		||||
             Otherwise we need to find all sets of connected nodes within the graph, and split them off into their own
 | 
			
		||||
             networks.
 | 
			
		||||
 | 
			
		||||
             With our current graph representation[^1], this requires a traversal of the graph, taking O(|V| + |E))
 | 
			
		||||
             time, which can get quite expensive for large graphs. We try to avoid this traversal where possible, by
 | 
			
		||||
             optimising for the case where the graph remains fully connected after removing this node, for instance,
 | 
			
		||||
             removing "A" here:
 | 
			
		||||
 | 
			
		||||
               A---B            B
 | 
			
		||||
               |   |    =>      |
 | 
			
		||||
               C---D        C---D
 | 
			
		||||
 | 
			
		||||
             We observe that these sorts of loops tend to be local, and so try to identify them as quickly as possible.
 | 
			
		||||
             To do this, we do a standard breadth-first traversal of the graph starting at the neighbours of the
 | 
			
		||||
             removed node, building sets of connected nodes.
 | 
			
		||||
 | 
			
		||||
             If, at any point, all nodes visited so far are connected to each other, then we know all remaining nodes
 | 
			
		||||
             will also be connected. This allows us to abort our traversal of the graph, and just remove the node (much
 | 
			
		||||
             like we do in the single neighbour case above).
 | 
			
		||||
 | 
			
		||||
             Otherwise, we then just create a new network for each disjoint set of connected nodes.
 | 
			
		||||
 | 
			
		||||
             {^1]:
 | 
			
		||||
               There are efficient (near-logarithmic) algorithms for this (e.g. https://arxiv.org/pdf/1609.05867.pdf),
 | 
			
		||||
               but they are significantly more complex to implement.
 | 
			
		||||
            */
 | 
			
		||||
 | 
			
		||||
            // Create a new set of nodes for each neighbour, and add them to our queue of nodes to visit.
 | 
			
		||||
            List<WiredNodeImpl> queue = new ArrayList<>();
 | 
			
		||||
            Set<NodeSet> nodeSets = new HashSet<>(neighbours.size());
 | 
			
		||||
            for (var neighbour : neighbours) {
 | 
			
		||||
                nodeSets.add(neighbour.currentSet = new NodeSet());
 | 
			
		||||
                queue.add(neighbour);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Perform a breadth-first search of the graph, starting from the neighbours.
 | 
			
		||||
            graphSearch:
 | 
			
		||||
            for (var i = 0; i < queue.size(); i++) {
 | 
			
		||||
                var enqueuedNode = queue.get(i);
 | 
			
		||||
                for (var neighbour : enqueuedNode.neighbours) {
 | 
			
		||||
                    var nodeSet = Nullability.assertNonNull(enqueuedNode.currentSet).find();
 | 
			
		||||
 | 
			
		||||
                    // The neighbour has no set and so has not been visited yet. Add it to the current set and enqueue
 | 
			
		||||
                    // it to be visited.
 | 
			
		||||
                    if (neighbour.currentSet == null) {
 | 
			
		||||
                        nodeSet.addNode(neighbour);
 | 
			
		||||
                        queue.add(neighbour);
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Otherwise, take the union of the two nodes' sets if needed. If we've only got a single node set
 | 
			
		||||
                    // left, then we know the whole graph is network is connected (even if not all nodes have been
 | 
			
		||||
                    // visited) and so can abort early.
 | 
			
		||||
                    var neighbourSet = neighbour.currentSet.find();
 | 
			
		||||
                    if (nodeSet != neighbourSet) {
 | 
			
		||||
                        var removed = nodeSets.remove(NodeSet.merge(nodeSet, neighbourSet));
 | 
			
		||||
                        assert removed : "Merged set should have been ";
 | 
			
		||||
                        if (nodeSets.size() == 1) break graphSearch;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // If we have a single subset, then all nodes are reachable - just clear the set and exit.
 | 
			
		||||
            if (nodeSets.size() == 1) {
 | 
			
		||||
                assert nodeSets.iterator().next().size() == queue.size();
 | 
			
		||||
                for (var neighbour : queue) neighbour.currentSet = null;
 | 
			
		||||
 | 
			
		||||
            // If all nodes are reachable then exit.
 | 
			
		||||
            if (reachable.size() == nodes.size()) {
 | 
			
		||||
                // Broadcast our simple peripheral changes
 | 
			
		||||
                removeSingleNode(wired, wiredNetwork);
 | 
			
		||||
                InvariantChecker.checkNode(wired);
 | 
			
		||||
@@ -198,43 +265,46 @@ final class WiredNetworkImpl implements WiredNetwork {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // A split may cause 2..neighbours.size() separate networks, so we
 | 
			
		||||
            // iterate through our neighbour list, generating child networks.
 | 
			
		||||
            neighbours.removeAll(reachable);
 | 
			
		||||
            var maximals = new ArrayList<WiredNetworkImpl>(neighbours.size() + 1);
 | 
			
		||||
            maximals.add(wiredNetwork);
 | 
			
		||||
            maximals.add(new WiredNetworkImpl(reachable));
 | 
			
		||||
            assert queue.size() == nodes.size() : "Expected queue to contain all nodes.";
 | 
			
		||||
 | 
			
		||||
            while (!neighbours.isEmpty()) {
 | 
			
		||||
                reachable = reachableNodes(neighbours.iterator().next());
 | 
			
		||||
                neighbours.removeAll(reachable);
 | 
			
		||||
                maximals.add(new WiredNetworkImpl(reachable));
 | 
			
		||||
            // Otherwise we need to create our new networks.
 | 
			
		||||
            var networks = new ArrayList<WiredNetworkImpl>(1 + nodeSets.size());
 | 
			
		||||
            // Add the network we've created for the removed node.
 | 
			
		||||
            networks.add(wiredNetwork);
 | 
			
		||||
            //  And then create a new network for each disjoint subset.
 | 
			
		||||
            for (var set : nodeSets) {
 | 
			
		||||
                var network = new WiredNetworkImpl(new HashSet<>(set.size()));
 | 
			
		||||
                set.setNetwork(network);
 | 
			
		||||
                networks.add(network);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            for (var network : maximals) network.lock.writeLock().lock();
 | 
			
		||||
            for (var network : networks) network.lock.writeLock().lock();
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                // We special case the original node: detaching all peripherals when needed.
 | 
			
		||||
                wired.network = wiredNetwork;
 | 
			
		||||
                wired.peripherals = Map.of();
 | 
			
		||||
                wired.neighbours.clear();
 | 
			
		||||
 | 
			
		||||
                // Ensure every network is finalised
 | 
			
		||||
                for (var network : maximals) {
 | 
			
		||||
                    for (var child : network.nodes) {
 | 
			
		||||
                        child.network = network;
 | 
			
		||||
                        network.peripherals.putAll(child.peripherals);
 | 
			
		||||
                    }
 | 
			
		||||
                // Add all nodes to their appropriate network.
 | 
			
		||||
                for (var child : queue) {
 | 
			
		||||
                    var network = Nullability.assertNonNull(child.currentSet).network();
 | 
			
		||||
                    child.currentSet = null;
 | 
			
		||||
 | 
			
		||||
                    child.network = network;
 | 
			
		||||
                    network.nodes.add(child);
 | 
			
		||||
                    network.peripherals.putAll(child.peripherals);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                for (var network : maximals) InvariantChecker.checkNetwork(network);
 | 
			
		||||
                for (var network : networks) InvariantChecker.checkNetwork(network);
 | 
			
		||||
                InvariantChecker.checkNode(wired);
 | 
			
		||||
 | 
			
		||||
                // Then broadcast network changes once all nodes are finalised
 | 
			
		||||
                for (var network : maximals) {
 | 
			
		||||
                for (var network : networks) {
 | 
			
		||||
                    WiredNetworkChangeImpl.changeOf(peripherals, network.peripherals).broadcast(network.nodes);
 | 
			
		||||
                }
 | 
			
		||||
            } finally {
 | 
			
		||||
                for (var network : maximals) network.lock.writeLock().unlock();
 | 
			
		||||
                for (var network : networks) network.lock.writeLock().unlock();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            nodes.clear();
 | 
			
		||||
@@ -373,22 +443,4 @@ final class WiredNetworkImpl implements WiredNetwork {
 | 
			
		||||
            throw new IllegalArgumentException("Unknown implementation of IWiredNode: " + node);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static Set<WiredNodeImpl> reachableNodes(WiredNodeImpl start) {
 | 
			
		||||
        Queue<WiredNodeImpl> enqueued = new ArrayDeque<>();
 | 
			
		||||
        var reachable = new HashSet<WiredNodeImpl>();
 | 
			
		||||
 | 
			
		||||
        reachable.add(start);
 | 
			
		||||
        enqueued.add(start);
 | 
			
		||||
 | 
			
		||||
        WiredNodeImpl node;
 | 
			
		||||
        while ((node = enqueued.poll()) != null) {
 | 
			
		||||
            for (var neighbour : node.neighbours) {
 | 
			
		||||
                // Otherwise attempt to enqueue this neighbour as well.
 | 
			
		||||
                if (reachable.add(neighbour)) enqueued.add(neighbour);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return reachable;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -27,11 +27,39 @@ public final class WiredNodeImpl implements WiredNode {
 | 
			
		||||
    final HashSet<WiredNodeImpl> neighbours = new HashSet<>();
 | 
			
		||||
    volatile WiredNetworkImpl network;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A temporary field used when checking network connectivity.
 | 
			
		||||
     *
 | 
			
		||||
     * @see WiredNetworkImpl#remove(WiredNode)
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    NodeSet currentSet;
 | 
			
		||||
 | 
			
		||||
    public WiredNodeImpl(WiredElement element) {
 | 
			
		||||
        this.element = element;
 | 
			
		||||
        network = new WiredNetworkImpl(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean connectTo(WiredNode node) {
 | 
			
		||||
        return network.connect(this, node);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean disconnectFrom(WiredNode node) {
 | 
			
		||||
        return network == ((WiredNodeImpl) node).network && network.disconnect(this, node);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean remove() {
 | 
			
		||||
        return network.remove(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void updatePeripherals(Map<String, IPeripheral> peripherals) {
 | 
			
		||||
        network.updatePeripherals(this, peripherals);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public synchronized void addReceiver(PacketReceiver receiver) {
 | 
			
		||||
        if (receivers == null) receivers = new HashSet<>();
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -14,12 +14,16 @@ import dan200.computercraft.shared.computer.metrics.ComputerMBean;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.monitor.MonitorWatcher;
 | 
			
		||||
import dan200.computercraft.shared.util.DropConsumer;
 | 
			
		||||
import dan200.computercraft.shared.util.TickScheduler;
 | 
			
		||||
import net.minecraft.resources.ResourceKey;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.server.MinecraftServer;
 | 
			
		||||
import net.minecraft.server.dedicated.DedicatedServer;
 | 
			
		||||
import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
import net.minecraft.server.level.ServerPlayer;
 | 
			
		||||
import net.minecraft.server.packs.resources.PreparableReloadListener;
 | 
			
		||||
import net.minecraft.world.entity.Entity;
 | 
			
		||||
import net.minecraft.world.item.CreativeModeTab;
 | 
			
		||||
import net.minecraft.world.item.CreativeModeTabs;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.level.chunk.LevelChunk;
 | 
			
		||||
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
 | 
			
		||||
@@ -69,10 +73,19 @@ public final class CommonHooks {
 | 
			
		||||
        NetworkUtils.reset();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void onServerChunkUnload(LevelChunk chunk) {
 | 
			
		||||
        if (!(chunk.getLevel() instanceof ServerLevel)) throw new IllegalArgumentException("Not a server chunk.");
 | 
			
		||||
        TickScheduler.onChunkUnload(chunk);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void onChunkWatch(LevelChunk chunk, ServerPlayer player) {
 | 
			
		||||
        MonitorWatcher.onWatch(chunk, player);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void onChunkTicketLevelChanged(ServerLevel level, long chunkPos, int oldLevel, int newLevel) {
 | 
			
		||||
        TickScheduler.onChunkTicketChanged(level, chunkPos, oldLevel, newLevel);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static final ResourceLocation TREASURE_DISK_LOOT = new ResourceLocation(ComputerCraftAPI.MOD_ID, "treasure_disk");
 | 
			
		||||
 | 
			
		||||
    private static final Set<ResourceLocation> TREASURE_DISK_LOOT_TABLES = Set.of(
 | 
			
		||||
@@ -111,4 +124,17 @@ public final class CommonHooks {
 | 
			
		||||
    public static boolean onLivingDrop(Entity entity, ItemStack stack) {
 | 
			
		||||
        return DropConsumer.onLivingDrop(entity, stack);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add items to an existing creative tab.
 | 
			
		||||
     *
 | 
			
		||||
     * @param key     The {@link ResourceKey} for this creative tab.
 | 
			
		||||
     * @param context Additional parameters used for building the contents.
 | 
			
		||||
     * @param out     The creative tab output to append items to.
 | 
			
		||||
     */
 | 
			
		||||
    public static void onBuildCreativeTab(ResourceKey<CreativeModeTab> key, CreativeModeTab.ItemDisplayParameters context, CreativeModeTab.Output out) {
 | 
			
		||||
        if (key == CreativeModeTabs.OP_BLOCKS && context.hasPermissions()) {
 | 
			
		||||
            out.accept(ModRegistry.Items.COMPUTER_COMMAND.get());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,7 @@ import dan200.computercraft.shared.computer.inventory.ViewComputerMenu;
 | 
			
		||||
import dan200.computercraft.shared.computer.items.CommandComputerItem;
 | 
			
		||||
import dan200.computercraft.shared.computer.items.ComputerItem;
 | 
			
		||||
import dan200.computercraft.shared.computer.recipe.ComputerUpgradeRecipe;
 | 
			
		||||
import dan200.computercraft.shared.config.Config;
 | 
			
		||||
import dan200.computercraft.shared.data.BlockNamedEntityLootCondition;
 | 
			
		||||
import dan200.computercraft.shared.data.ConstantLootConditionSerializer;
 | 
			
		||||
import dan200.computercraft.shared.data.HasComputerIdLootCondition;
 | 
			
		||||
@@ -139,19 +140,17 @@ public final class ModRegistry {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static final RegistryEntry<ComputerBlock<ComputerBlockEntity>> COMPUTER_NORMAL = REGISTRY.register("computer_normal",
 | 
			
		||||
            () -> new ComputerBlock<>(computerProperties().mapColor(MapColor.STONE), ComputerFamily.NORMAL, BlockEntities.COMPUTER_NORMAL));
 | 
			
		||||
            () -> new ComputerBlock<>(computerProperties().mapColor(MapColor.STONE), BlockEntities.COMPUTER_NORMAL));
 | 
			
		||||
        public static final RegistryEntry<ComputerBlock<ComputerBlockEntity>> COMPUTER_ADVANCED = REGISTRY.register("computer_advanced",
 | 
			
		||||
            () -> new ComputerBlock<>(computerProperties().mapColor(MapColor.GOLD), ComputerFamily.ADVANCED, BlockEntities.COMPUTER_ADVANCED));
 | 
			
		||||
            () -> new ComputerBlock<>(computerProperties().mapColor(MapColor.GOLD), BlockEntities.COMPUTER_ADVANCED));
 | 
			
		||||
 | 
			
		||||
        public static final RegistryEntry<ComputerBlock<CommandComputerBlockEntity>> COMPUTER_COMMAND = REGISTRY.register("computer_command", () -> new CommandComputerBlock<>(
 | 
			
		||||
            computerProperties().strength(-1, 6000000.0F),
 | 
			
		||||
            ComputerFamily.COMMAND, BlockEntities.COMPUTER_COMMAND
 | 
			
		||||
        ));
 | 
			
		||||
        public static final RegistryEntry<ComputerBlock<CommandComputerBlockEntity>> COMPUTER_COMMAND = REGISTRY.register("computer_command",
 | 
			
		||||
            () -> new CommandComputerBlock<>(computerProperties().strength(-1, 6000000.0F), BlockEntities.COMPUTER_COMMAND));
 | 
			
		||||
 | 
			
		||||
        public static final RegistryEntry<TurtleBlock> TURTLE_NORMAL = REGISTRY.register("turtle_normal",
 | 
			
		||||
            () -> new TurtleBlock(turtleProperties().mapColor(MapColor.STONE), ComputerFamily.NORMAL, BlockEntities.TURTLE_NORMAL));
 | 
			
		||||
            () -> new TurtleBlock(turtleProperties().mapColor(MapColor.STONE), BlockEntities.TURTLE_NORMAL));
 | 
			
		||||
        public static final RegistryEntry<TurtleBlock> TURTLE_ADVANCED = REGISTRY.register("turtle_advanced",
 | 
			
		||||
            () -> new TurtleBlock(turtleProperties().mapColor(MapColor.GOLD), ComputerFamily.ADVANCED, BlockEntities.TURTLE_ADVANCED));
 | 
			
		||||
            () -> new TurtleBlock(turtleProperties().mapColor(MapColor.GOLD).explosionResistance(TurtleBlock.IMMUNE_EXPLOSION_RESISTANCE), BlockEntities.TURTLE_ADVANCED));
 | 
			
		||||
 | 
			
		||||
        public static final RegistryEntry<SpeakerBlock> SPEAKER = REGISTRY.register("speaker", () -> new SpeakerBlock(properties().mapColor(MapColor.STONE)));
 | 
			
		||||
        public static final RegistryEntry<DiskDriveBlock> DISK_DRIVE = REGISTRY.register("disk_drive", () -> new DiskDriveBlock(properties().mapColor(MapColor.STONE)));
 | 
			
		||||
@@ -192,9 +191,9 @@ public final class ModRegistry {
 | 
			
		||||
            ofBlock(Blocks.COMPUTER_COMMAND, (p, s) -> new CommandComputerBlockEntity(BlockEntities.COMPUTER_COMMAND.get(), p, s));
 | 
			
		||||
 | 
			
		||||
        public static final RegistryEntry<BlockEntityType<TurtleBlockEntity>> TURTLE_NORMAL =
 | 
			
		||||
            ofBlock(Blocks.TURTLE_NORMAL, (p, s) -> new TurtleBlockEntity(BlockEntities.TURTLE_NORMAL.get(), p, s, ComputerFamily.NORMAL));
 | 
			
		||||
            ofBlock(Blocks.TURTLE_NORMAL, (p, s) -> new TurtleBlockEntity(BlockEntities.TURTLE_NORMAL.get(), p, s, () -> Config.turtleFuelLimit, ComputerFamily.NORMAL));
 | 
			
		||||
        public static final RegistryEntry<BlockEntityType<TurtleBlockEntity>> TURTLE_ADVANCED =
 | 
			
		||||
            ofBlock(Blocks.TURTLE_ADVANCED, (p, s) -> new TurtleBlockEntity(BlockEntities.TURTLE_ADVANCED.get(), p, s, ComputerFamily.ADVANCED));
 | 
			
		||||
            ofBlock(Blocks.TURTLE_ADVANCED, (p, s) -> new TurtleBlockEntity(BlockEntities.TURTLE_ADVANCED.get(), p, s, () -> Config.advancedTurtleFuelLimit, ComputerFamily.ADVANCED));
 | 
			
		||||
 | 
			
		||||
        public static final RegistryEntry<BlockEntityType<SpeakerBlockEntity>> SPEAKER =
 | 
			
		||||
            ofBlock(Blocks.SPEAKER, (p, s) -> new SpeakerBlockEntity(BlockEntities.SPEAKER.get(), p, s));
 | 
			
		||||
@@ -311,7 +310,10 @@ public final class ModRegistry {
 | 
			
		||||
            () -> new MenuType<>(PrinterMenu::new, FeatureFlags.VANILLA_SET));
 | 
			
		||||
 | 
			
		||||
        public static final RegistryEntry<MenuType<HeldItemMenu>> PRINTOUT = REGISTRY.register("printout",
 | 
			
		||||
            () -> ContainerData.toType(HeldItemContainerData::new, HeldItemMenu::createPrintout));
 | 
			
		||||
            () -> ContainerData.toType(
 | 
			
		||||
                HeldItemContainerData::new,
 | 
			
		||||
                (id, inventory, data) -> new HeldItemMenu(Menus.PRINTOUT.get(), id, inventory.player, data.getHand())
 | 
			
		||||
            ));
 | 
			
		||||
 | 
			
		||||
        public static final RegistryEntry<MenuType<ViewComputerMenu>> VIEW_COMPUTER = REGISTRY.register("view_computer",
 | 
			
		||||
            () -> ContainerData.toType(ComputerContainerData::new, ViewComputerMenu::new));
 | 
			
		||||
@@ -371,11 +373,11 @@ public final class ModRegistry {
 | 
			
		||||
        public static final RegistryEntry<SimpleCraftingRecipeSerializer<ClearColourRecipe>> DYEABLE_ITEM_CLEAR = simple("clear_colour", ClearColourRecipe::new);
 | 
			
		||||
        public static final RegistryEntry<RecipeSerializer<TurtleRecipe>> TURTLE = REGISTRY.register("turtle", () -> TurtleRecipe.validatingSerialiser(TurtleRecipe::of));
 | 
			
		||||
        public static final RegistryEntry<SimpleCraftingRecipeSerializer<TurtleUpgradeRecipe>> TURTLE_UPGRADE = simple("turtle_upgrade", TurtleUpgradeRecipe::new);
 | 
			
		||||
        public static final RegistryEntry<RecipeSerializer<TurtleOverlayRecipe>> TURTLE_OVERLAY = REGISTRY.register("turtle_overlay", TurtleOverlayRecipe.Serializer::new);
 | 
			
		||||
        public static final RegistryEntry<RecipeSerializer<TurtleOverlayRecipe>> TURTLE_OVERLAY = REGISTRY.register("turtle_overlay", TurtleOverlayRecipe.Serialiser::new);
 | 
			
		||||
        public static final RegistryEntry<SimpleCraftingRecipeSerializer<PocketComputerUpgradeRecipe>> POCKET_COMPUTER_UPGRADE = simple("pocket_computer_upgrade", PocketComputerUpgradeRecipe::new);
 | 
			
		||||
        public static final RegistryEntry<SimpleCraftingRecipeSerializer<PrintoutRecipe>> PRINTOUT = simple("printout", PrintoutRecipe::new);
 | 
			
		||||
        public static final RegistryEntry<SimpleCraftingRecipeSerializer<DiskRecipe>> DISK = simple("disk", DiskRecipe::new);
 | 
			
		||||
        public static final RegistryEntry<RecipeSerializer<ComputerUpgradeRecipe>> COMPUTER_UPGRADE = REGISTRY.register("computer_upgrade", ComputerUpgradeRecipe.Serializer::new);
 | 
			
		||||
        public static final RegistryEntry<RecipeSerializer<ComputerUpgradeRecipe>> COMPUTER_UPGRADE = REGISTRY.register("computer_upgrade", () -> CustomShapedRecipe.validatingSerialiser(ComputerUpgradeRecipe::of));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static class Permissions {
 | 
			
		||||
@@ -464,8 +466,8 @@ public final class ModRegistry {
 | 
			
		||||
     * Register any objects which must be done on the main thread.
 | 
			
		||||
     */
 | 
			
		||||
    public static void registerMainThread() {
 | 
			
		||||
        CauldronInteraction.WATER.put(ModRegistry.Items.TURTLE_NORMAL.get(), TurtleItem.CAULDRON_INTERACTION);
 | 
			
		||||
        CauldronInteraction.WATER.put(ModRegistry.Items.TURTLE_ADVANCED.get(), TurtleItem.CAULDRON_INTERACTION);
 | 
			
		||||
        CauldronInteraction.WATER.put(Items.TURTLE_NORMAL.get(), TurtleItem.CAULDRON_INTERACTION);
 | 
			
		||||
        CauldronInteraction.WATER.put(Items.TURTLE_ADVANCED.get(), TurtleItem.CAULDRON_INTERACTION);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void addTurtle(CreativeModeTab.Output out, TurtleItem turtle) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,6 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.common;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import dan200.computercraft.shared.network.container.HeldItemContainerData;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.world.InteractionHand;
 | 
			
		||||
import net.minecraft.world.MenuProvider;
 | 
			
		||||
@@ -28,10 +26,6 @@ public class HeldItemMenu extends AbstractContainerMenu {
 | 
			
		||||
        stack = player.getItemInHand(hand).copy();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static HeldItemMenu createPrintout(int id, Inventory inventory, HeldItemContainerData data) {
 | 
			
		||||
        return new HeldItemMenu(ModRegistry.Menus.PRINTOUT.get(), id, inventory.player, data.getHand());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ItemStack getStack() {
 | 
			
		||||
        return stack;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ package dan200.computercraft.shared.computer.blocks;
 | 
			
		||||
import dan200.computercraft.annotations.ForgeOverride;
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.shared.common.IBundledRedstoneBlock;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import dan200.computercraft.shared.computer.items.IComputerItem;
 | 
			
		||||
import dan200.computercraft.shared.platform.RegistryEntry;
 | 
			
		||||
import dan200.computercraft.shared.util.BlockEntityHelpers;
 | 
			
		||||
@@ -42,13 +41,11 @@ import javax.annotation.Nullable;
 | 
			
		||||
public abstract class AbstractComputerBlock<T extends AbstractComputerBlockEntity> extends HorizontalDirectionalBlock implements IBundledRedstoneBlock, EntityBlock {
 | 
			
		||||
    private static final ResourceLocation DROP = new ResourceLocation(ComputerCraftAPI.MOD_ID, "computer");
 | 
			
		||||
 | 
			
		||||
    private final ComputerFamily family;
 | 
			
		||||
    protected final RegistryEntry<BlockEntityType<T>> type;
 | 
			
		||||
    private final BlockEntityTicker<T> serverTicker = (level, pos, state, computer) -> computer.serverTick();
 | 
			
		||||
 | 
			
		||||
    protected AbstractComputerBlock(Properties settings, ComputerFamily family, RegistryEntry<BlockEntityType<T>> type) {
 | 
			
		||||
    protected AbstractComputerBlock(Properties settings, RegistryEntry<BlockEntityType<T>> type) {
 | 
			
		||||
        super(settings);
 | 
			
		||||
        this.family = family;
 | 
			
		||||
        this.type = type;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -82,10 +79,6 @@ public abstract class AbstractComputerBlock<T extends AbstractComputerBlockEntit
 | 
			
		||||
 | 
			
		||||
    protected abstract ItemStack getItem(AbstractComputerBlockEntity tile);
 | 
			
		||||
 | 
			
		||||
    public ComputerFamily getFamily() {
 | 
			
		||||
        return family;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    @Deprecated
 | 
			
		||||
    public int getSignal(BlockState state, BlockGetter world, BlockPos pos, Direction incomingSide) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
@@ -114,16 +113,13 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
 | 
			
		||||
        return InteractionResult.PASS;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void neighborChanged(BlockPos neighbour) {
 | 
			
		||||
        updateInputAt(neighbour);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected void serverTick() {
 | 
			
		||||
        if (getLevel().isClientSide) return;
 | 
			
		||||
        if (computerID < 0 && !startOn) return; // Don't tick if we don't need a computer!
 | 
			
		||||
 | 
			
		||||
        var computer = createServerComputer();
 | 
			
		||||
 | 
			
		||||
        // Update any peripherals that have changed.
 | 
			
		||||
        if (invalidSides != 0) {
 | 
			
		||||
            for (var direction : DirectionUtil.FACINGS) {
 | 
			
		||||
                if ((invalidSides & (1 << direction.ordinal())) != 0) refreshPeripheral(computer, direction);
 | 
			
		||||
@@ -140,16 +136,30 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
 | 
			
		||||
 | 
			
		||||
        fresh = false;
 | 
			
		||||
        computerID = computer.getID();
 | 
			
		||||
        label = computer.getLabel();
 | 
			
		||||
        on = computer.isOn();
 | 
			
		||||
 | 
			
		||||
        // Update the block state if needed. We don't fire a block update intentionally,
 | 
			
		||||
        // as this only really is needed on the client side.
 | 
			
		||||
        // If the on state has changed, mark as as dirty.
 | 
			
		||||
        var newOn = computer.isOn();
 | 
			
		||||
        if (on != newOn) {
 | 
			
		||||
            on = newOn;
 | 
			
		||||
            setChanged();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If the label has changed, mark as dirty and sync to client.
 | 
			
		||||
        var newLabel = computer.getLabel();
 | 
			
		||||
        if (!Objects.equals(label, newLabel)) {
 | 
			
		||||
            label = newLabel;
 | 
			
		||||
            BlockEntityHelpers.updateBlock(this);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Update the block state if needed.
 | 
			
		||||
        updateBlockState(computer.getState());
 | 
			
		||||
 | 
			
		||||
        // TODO: This should ideally be split up into label/id/on (which should save NBT and sync to client) and
 | 
			
		||||
        //  redstone (which should update outputs)
 | 
			
		||||
        if (computer.hasOutputChanged()) updateOutput();
 | 
			
		||||
        var changes = computer.pollAndResetChanges();
 | 
			
		||||
        if (changes != 0) {
 | 
			
		||||
            for (var direction : DirectionUtil.FACINGS) {
 | 
			
		||||
                if ((changes & (1 << remapToLocalSide(direction).ordinal())) != 0) updateRedstoneTo(direction);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected abstract void updateBlockState(ComputerState newState);
 | 
			
		||||
@@ -199,11 +209,15 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
 | 
			
		||||
        return localSide;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void updateRedstoneInputs(ServerComputer computer) {
 | 
			
		||||
        var pos = getBlockPos();
 | 
			
		||||
        for (var dir : DirectionUtil.FACINGS) updateRedstoneInput(computer, dir, pos.relative(dir));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the redstone input on a particular side.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * This is called <em>immediately</em> when a neighbouring block changes (see {@link #neighborChanged(BlockPos)}).
 | 
			
		||||
     *
 | 
			
		||||
     * @param computer  The current server computer.
 | 
			
		||||
     * @param dir       The direction to update in.
 | 
			
		||||
     * @param targetPos The position of the adjacent block, equal to {@code getBlockPos().offset(dir)}.
 | 
			
		||||
     */
 | 
			
		||||
    private void updateRedstoneInput(ServerComputer computer, Direction dir, BlockPos targetPos) {
 | 
			
		||||
        var offsetSide = dir.getOpposite();
 | 
			
		||||
        var localDir = remapToLocalSide(dir);
 | 
			
		||||
@@ -212,13 +226,22 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
 | 
			
		||||
        computer.setBundledRedstoneInput(localDir, BundledRedstone.getOutput(getLevel(), targetPos, offsetSide));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the peripheral on a particular side.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * This is called from {@link #serverTick()}, after a peripheral has been marked as invalid (such as in
 | 
			
		||||
     * {@link #neighborChanged(BlockPos)})
 | 
			
		||||
     *
 | 
			
		||||
     * @param computer The current server computer.
 | 
			
		||||
     * @param dir      The direction to update in.
 | 
			
		||||
     */
 | 
			
		||||
    private void refreshPeripheral(ServerComputer computer, Direction dir) {
 | 
			
		||||
        invalidSides &= ~(1 << dir.ordinal());
 | 
			
		||||
 | 
			
		||||
        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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -244,7 +267,18 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void updateInputAt(BlockPos neighbour) {
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when a neighbour block changes.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * This finds the side the neighbour block is on, and updates the inputs accordingly.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * We do <strong>NOT</strong> update the peripheral immediately. Blocks and block entities are sometimes
 | 
			
		||||
     * inconsistent at the point where an update is received, and so we instead just mark that side as dirty (see
 | 
			
		||||
     * {@link #invalidSides}) and refresh it {@linkplain #serverTick() next tick}.
 | 
			
		||||
     *
 | 
			
		||||
     * @param neighbour The position of the neighbour block.
 | 
			
		||||
     */
 | 
			
		||||
    public void neighborChanged(BlockPos neighbour) {
 | 
			
		||||
        var computer = getServerComputer();
 | 
			
		||||
        if (computer == null) return;
 | 
			
		||||
 | 
			
		||||
@@ -259,22 +293,28 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
 | 
			
		||||
 | 
			
		||||
        // If the position is not any adjacent one, update all inputs. This is pretty terrible, but some redstone mods
 | 
			
		||||
        // handle this incorrectly.
 | 
			
		||||
        updateRedstoneInputs(computer);
 | 
			
		||||
        for (var dir : DirectionUtil.FACINGS) updateRedstoneInput(computer, dir, getBlockPos().relative(dir));
 | 
			
		||||
        invalidSides = (1 << 6) - 1; // Mark all peripherals as dirty.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the block's state and propagate redstone output.
 | 
			
		||||
     * Update outputs in a specific direction.
 | 
			
		||||
     *
 | 
			
		||||
     * @param direction The direction to propagate outputs in.
 | 
			
		||||
     */
 | 
			
		||||
    public void updateOutput() {
 | 
			
		||||
        BlockEntityHelpers.updateBlock(this);
 | 
			
		||||
        for (var dir : DirectionUtil.FACINGS) RedstoneUtil.propagateRedstoneOutput(getLevel(), getBlockPos(), dir);
 | 
			
		||||
    protected void updateRedstoneTo(Direction direction) {
 | 
			
		||||
        RedstoneUtil.propagateRedstoneOutput(getLevel(), getBlockPos(), direction);
 | 
			
		||||
 | 
			
		||||
        var computer = getServerComputer();
 | 
			
		||||
        if (computer != null) updateRedstoneInputs(computer);
 | 
			
		||||
        if (computer != null) updateRedstoneInput(computer, direction, getBlockPos().relative(direction));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected abstract ServerComputer createComputer(int id);
 | 
			
		||||
    /**
 | 
			
		||||
     * Update all redstone outputs.
 | 
			
		||||
     */
 | 
			
		||||
    public void updateRedstone() {
 | 
			
		||||
        for (var dir : DirectionUtil.FACINGS) updateRedstoneTo(dir);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final int getComputerID() {
 | 
			
		||||
@@ -332,6 +372,8 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
 | 
			
		||||
        return computer;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected abstract ServerComputer createComputer(int id);
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public ServerComputer getServerComputer() {
 | 
			
		||||
        return getLevel().isClientSide || getLevel().getServer() == null ? null : ServerContext.get(getLevel().getServer()).registry().get(instanceID);
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.computer.blocks;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import dan200.computercraft.shared.platform.RegistryEntry;
 | 
			
		||||
import net.minecraft.world.level.block.GameMasterBlock;
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntityType;
 | 
			
		||||
@@ -17,7 +16,7 @@ import net.minecraft.world.level.block.entity.BlockEntityType;
 | 
			
		||||
 * @see dan200.computercraft.shared.computer.items.CommandComputerItem
 | 
			
		||||
 */
 | 
			
		||||
public class CommandComputerBlock<T extends CommandComputerBlockEntity> extends ComputerBlock<T> implements GameMasterBlock {
 | 
			
		||||
    public CommandComputerBlock(Properties settings, ComputerFamily family, RegistryEntry<BlockEntityType<T>> type) {
 | 
			
		||||
        super(settings, family, type);
 | 
			
		||||
    public CommandComputerBlock(Properties settings, RegistryEntry<BlockEntityType<T>> type) {
 | 
			
		||||
        super(settings, type);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -4,9 +4,8 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.computer.blocks;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerState;
 | 
			
		||||
import dan200.computercraft.shared.computer.items.ComputerItemFactory;
 | 
			
		||||
import dan200.computercraft.shared.computer.items.ComputerItem;
 | 
			
		||||
import dan200.computercraft.shared.platform.RegistryEntry;
 | 
			
		||||
import net.minecraft.core.Direction;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
@@ -25,8 +24,8 @@ public class ComputerBlock<T extends ComputerBlockEntity> extends AbstractComput
 | 
			
		||||
    public static final EnumProperty<ComputerState> STATE = EnumProperty.create("state", ComputerState.class);
 | 
			
		||||
    public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING;
 | 
			
		||||
 | 
			
		||||
    public ComputerBlock(Properties settings, ComputerFamily family, RegistryEntry<BlockEntityType<T>> type) {
 | 
			
		||||
        super(settings, family, type);
 | 
			
		||||
    public ComputerBlock(Properties settings, RegistryEntry<BlockEntityType<T>> type) {
 | 
			
		||||
        super(settings, type);
 | 
			
		||||
        registerDefaultState(defaultBlockState()
 | 
			
		||||
            .setValue(FACING, Direction.NORTH)
 | 
			
		||||
            .setValue(STATE, ComputerState.OFF)
 | 
			
		||||
@@ -46,6 +45,9 @@ public class ComputerBlock<T extends ComputerBlockEntity> extends AbstractComput
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected ItemStack getItem(AbstractComputerBlockEntity tile) {
 | 
			
		||||
        return tile instanceof ComputerBlockEntity ? ComputerItemFactory.create((ComputerBlockEntity) tile) : ItemStack.EMPTY;
 | 
			
		||||
        if (!(tile instanceof ComputerBlockEntity computer)) return ItemStack.EMPTY;
 | 
			
		||||
        if (!(asItem() instanceof ComputerItem item)) return ItemStack.EMPTY;
 | 
			
		||||
 | 
			
		||||
        return item.create(computer.getComputerID(), computer.getLabel());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -32,11 +32,9 @@ public class ComputerBlockEntity extends AbstractComputerBlockEntity {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected ServerComputer createComputer(int id) {
 | 
			
		||||
        var family = getFamily();
 | 
			
		||||
        return new ServerComputer(
 | 
			
		||||
            (ServerLevel) getLevel(), getBlockPos(), id, label,
 | 
			
		||||
            family, Config.computerTermWidth,
 | 
			
		||||
            Config.computerTermHeight
 | 
			
		||||
            getFamily(), Config.computerTermWidth, Config.computerTermHeight
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -53,7 +51,7 @@ public class ComputerBlockEntity extends AbstractComputerBlockEntity {
 | 
			
		||||
    protected void updateBlockState(ComputerState newState) {
 | 
			
		||||
        var existing = getBlockState();
 | 
			
		||||
        if (existing.getValue(ComputerBlock.STATE) != newState) {
 | 
			
		||||
            getLevel().setBlock(getBlockPos(), existing.setValue(ComputerBlock.STATE, newState), 3);
 | 
			
		||||
            getLevel().setBlock(getBlockPos(), existing.setValue(ComputerBlock.STATE, newState), ComputerBlock.UPDATE_CLIENTS);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,33 +4,8 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.computer.core;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import com.google.gson.JsonSyntaxException;
 | 
			
		||||
import net.minecraft.util.GsonHelper;
 | 
			
		||||
import net.minecraft.util.StringRepresentable;
 | 
			
		||||
 | 
			
		||||
public enum ComputerFamily implements StringRepresentable {
 | 
			
		||||
    NORMAL("normal"),
 | 
			
		||||
    ADVANCED("advanced"),
 | 
			
		||||
    COMMAND("command");
 | 
			
		||||
 | 
			
		||||
    private final String name;
 | 
			
		||||
 | 
			
		||||
    ComputerFamily(String name) {
 | 
			
		||||
        this.name = name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static ComputerFamily getFamily(JsonObject json, String name) {
 | 
			
		||||
        var familyName = GsonHelper.getAsString(json, name);
 | 
			
		||||
        for (var family : values()) {
 | 
			
		||||
            if (family.getSerializedName().equalsIgnoreCase(familyName)) return family;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new JsonSyntaxException("Unknown computer family '" + familyName + "' for field " + name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getSerializedName() {
 | 
			
		||||
        return name;
 | 
			
		||||
    }
 | 
			
		||||
public enum ComputerFamily {
 | 
			
		||||
    NORMAL,
 | 
			
		||||
    ADVANCED,
 | 
			
		||||
    COMMAND,
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -42,7 +42,6 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
 | 
			
		||||
    private final NetworkedTerminal terminal;
 | 
			
		||||
    private final AtomicBoolean terminalChanged = new AtomicBoolean(false);
 | 
			
		||||
 | 
			
		||||
    private boolean changedLastFrame;
 | 
			
		||||
    private int ticksSincePing;
 | 
			
		||||
 | 
			
		||||
    public ServerComputer(
 | 
			
		||||
@@ -96,10 +95,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
 | 
			
		||||
 | 
			
		||||
    public void tickServer() {
 | 
			
		||||
        ticksSincePing++;
 | 
			
		||||
 | 
			
		||||
        computer.tick();
 | 
			
		||||
 | 
			
		||||
        changedLastFrame = computer.pollAndResetChanged();
 | 
			
		||||
        if (terminalChanged.getAndSet(false)) onTerminalChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -119,8 +115,8 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
 | 
			
		||||
        return ticksSincePing > 100;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean hasOutputChanged() {
 | 
			
		||||
        return changedLastFrame;
 | 
			
		||||
    public int pollAndResetChanges() {
 | 
			
		||||
        return computer.pollAndResetChanges();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public int register() {
 | 
			
		||||
@@ -142,7 +138,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);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -167,7 +163,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ComputerState getState() {
 | 
			
		||||
        if (!isOn()) return ComputerState.OFF;
 | 
			
		||||
        if (!computer.isOn()) return ComputerState.OFF;
 | 
			
		||||
        return computer.isBlinking() ? ComputerState.BLINKING : ComputerState.ON;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,6 @@ import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.api.filesystem.Mount;
 | 
			
		||||
import dan200.computercraft.api.media.IMedia;
 | 
			
		||||
import dan200.computercraft.shared.computer.blocks.AbstractComputerBlock;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import dan200.computercraft.shared.config.Config;
 | 
			
		||||
import net.minecraft.ChatFormatting;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
@@ -22,11 +21,8 @@ import javax.annotation.Nullable;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
public abstract class AbstractComputerItem extends BlockItem implements IComputerItem, IMedia {
 | 
			
		||||
    private final ComputerFamily family;
 | 
			
		||||
 | 
			
		||||
    public AbstractComputerItem(AbstractComputerBlock<?> block, Properties settings) {
 | 
			
		||||
        super(block, settings);
 | 
			
		||||
        family = block.getFamily();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -45,13 +41,6 @@ public abstract class AbstractComputerItem extends BlockItem implements ICompute
 | 
			
		||||
        return IComputerItem.super.getLabel(stack);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final ComputerFamily getFamily() {
 | 
			
		||||
        return family;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // IMedia implementation
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean setLabel(ItemStack stack, @Nullable String label) {
 | 
			
		||||
        if (label != null) {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,8 @@
 | 
			
		||||
package dan200.computercraft.shared.computer.items;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.computer.blocks.ComputerBlock;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.world.item.Item;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
@@ -24,9 +24,9 @@ public class ComputerItem extends AbstractComputerItem {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public ItemStack withFamily(ItemStack stack, ComputerFamily family) {
 | 
			
		||||
        var result = ComputerItemFactory.create(getComputerID(stack), null, family);
 | 
			
		||||
        if (stack.hasCustomHoverName()) result.setHoverName(stack.getHoverName());
 | 
			
		||||
        return result;
 | 
			
		||||
    public ItemStack changeItem(ItemStack stack, Item newItem) {
 | 
			
		||||
        return newItem instanceof ComputerItem computer
 | 
			
		||||
            ? computer.create(getComputerID(stack), getLabel(stack))
 | 
			
		||||
            : ItemStack.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,29 +0,0 @@
 | 
			
		||||
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: LicenseRef-CCPL
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.computer.items;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import dan200.computercraft.shared.computer.blocks.ComputerBlockEntity;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
public final class ComputerItemFactory {
 | 
			
		||||
    private ComputerItemFactory() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static ItemStack create(ComputerBlockEntity tile) {
 | 
			
		||||
        return create(tile.getComputerID(), tile.getLabel(), tile.getFamily());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static ItemStack create(int id, @Nullable String label, ComputerFamily family) {
 | 
			
		||||
        return switch (family) {
 | 
			
		||||
            case NORMAL -> ModRegistry.Items.COMPUTER_NORMAL.get().create(id, label);
 | 
			
		||||
            case ADVANCED -> ModRegistry.Items.COMPUTER_ADVANCED.get().create(id, label);
 | 
			
		||||
            case COMMAND -> ModRegistry.Items.COMPUTER_COMMAND.get().create(id, label);
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.computer.items;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import net.minecraft.world.item.Item;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
@@ -21,7 +21,15 @@ public interface IComputerItem {
 | 
			
		||||
        return stack.hasCustomHoverName() ? stack.getHoverName().getString() : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ComputerFamily getFamily();
 | 
			
		||||
 | 
			
		||||
    ItemStack withFamily(ItemStack stack, ComputerFamily family);
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a new stack, changing the underlying item.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * This should copy the computer's data to a different item of the same type (for instance, converting a normal
 | 
			
		||||
     * computer to an advanced one).
 | 
			
		||||
     *
 | 
			
		||||
     * @param stack   The current computer stack.
 | 
			
		||||
     * @param newItem The new item.
 | 
			
		||||
     * @return The new stack, possibly {@linkplain ItemStack#EMPTY empty} if {@code newItem} is of the same type.
 | 
			
		||||
     */
 | 
			
		||||
    ItemStack changeItem(ItemStack stack, Item newItem);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
                    }
 | 
			
		||||
                }),
 | 
			
		||||
        });
 | 
			
		||||
 
 | 
			
		||||
@@ -46,12 +46,14 @@ public final class GlobalMetrics {
 | 
			
		||||
     * Add a new global metrics observer. This will receive metrics data for all computers.
 | 
			
		||||
     *
 | 
			
		||||
     * @param tracker The observer to add.
 | 
			
		||||
     * @return Whether the observer was added. {@code false} if the observer was already registered.
 | 
			
		||||
     */
 | 
			
		||||
    public void addObserver(ComputerMetricsObserver tracker) {
 | 
			
		||||
    public boolean addObserver(ComputerMetricsObserver tracker) {
 | 
			
		||||
        synchronized (lock) {
 | 
			
		||||
            if (trackers.contains(tracker)) return;
 | 
			
		||||
            if (trackers.contains(tracker)) return false;
 | 
			
		||||
            trackers.add(tracker);
 | 
			
		||||
            enabled = true;
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -59,11 +61,13 @@ public final class GlobalMetrics {
 | 
			
		||||
     * Remove a previously-registered global metrics observer.
 | 
			
		||||
     *
 | 
			
		||||
     * @param tracker The observer to add.
 | 
			
		||||
     * @return Whether the observer was removed. {@code false} if the observer was not registered.
 | 
			
		||||
     */
 | 
			
		||||
    public void removeObserver(ComputerMetricsObserver tracker) {
 | 
			
		||||
    public boolean removeObserver(ComputerMetricsObserver tracker) {
 | 
			
		||||
        synchronized (lock) {
 | 
			
		||||
            trackers.remove(tracker);
 | 
			
		||||
            var changed = trackers.remove(tracker);
 | 
			
		||||
            enabled = !trackers.isEmpty();
 | 
			
		||||
            return changed;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import dan200.computercraft.shared.computer.core.ServerComputer;
 | 
			
		||||
import dan200.computercraft.shared.computer.metrics.ComputerMetricsObserver;
 | 
			
		||||
import dan200.computercraft.shared.computer.metrics.GlobalMetrics;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.concurrent.GuardedBy;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
@@ -21,29 +22,31 @@ import java.util.Map;
 | 
			
		||||
 */
 | 
			
		||||
public class BasicComputerMetricsObserver implements ComputerMetricsObserver {
 | 
			
		||||
    private final GlobalMetrics owner;
 | 
			
		||||
    private boolean tracking = false;
 | 
			
		||||
 | 
			
		||||
    @GuardedBy("this")
 | 
			
		||||
    private final List<ComputerMetrics> timings = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
    @GuardedBy("this")
 | 
			
		||||
    private final Map<ServerComputer, ComputerMetrics> timingLookup = new MapMaker().weakKeys().makeMap();
 | 
			
		||||
 | 
			
		||||
    public BasicComputerMetricsObserver(GlobalMetrics owner) {
 | 
			
		||||
        this.owner = owner;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public synchronized void start() {
 | 
			
		||||
        if (!tracking) owner.addObserver(this);
 | 
			
		||||
        tracking = true;
 | 
			
		||||
    public void start() {
 | 
			
		||||
        if (!owner.addObserver(this)) return;
 | 
			
		||||
 | 
			
		||||
        timings.clear();
 | 
			
		||||
        timingLookup.clear();
 | 
			
		||||
        synchronized (this) {
 | 
			
		||||
            timings.clear();
 | 
			
		||||
            timingLookup.clear();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public synchronized boolean stop() {
 | 
			
		||||
        if (!tracking) return false;
 | 
			
		||||
 | 
			
		||||
        owner.removeObserver(this);
 | 
			
		||||
        tracking = false;
 | 
			
		||||
        timingLookup.clear();
 | 
			
		||||
    public boolean stop() {
 | 
			
		||||
        if (!owner.removeObserver(this)) return false;
 | 
			
		||||
        synchronized (this) {
 | 
			
		||||
            timingLookup.clear();
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -57,6 +60,7 @@ public class BasicComputerMetricsObserver implements ComputerMetricsObserver {
 | 
			
		||||
        return new ArrayList<>(timings);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @GuardedBy("this")
 | 
			
		||||
    private ComputerMetrics getMetrics(ServerComputer computer) {
 | 
			
		||||
        var existing = timingLookup.get(computer);
 | 
			
		||||
        if (existing != null) return existing;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,57 +4,44 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.computer.recipe;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import com.mojang.serialization.DataResult;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import dan200.computercraft.shared.computer.items.IComputerItem;
 | 
			
		||||
import dan200.computercraft.shared.recipe.ShapedRecipeSpec;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.item.Item;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.crafting.RecipeSerializer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A recipe which "upgrades" a {@linkplain IComputerItem computer}, converting it from one {@linkplain ComputerFamily
 | 
			
		||||
 * family} to another.
 | 
			
		||||
 * A recipe which "upgrades" a {@linkplain IComputerItem computer}, converting to it a new item (for instance a normal
 | 
			
		||||
 * turtle to an advanced one).
 | 
			
		||||
 *
 | 
			
		||||
 * @see IComputerItem#changeItem(ItemStack, Item)
 | 
			
		||||
 */
 | 
			
		||||
public final class ComputerUpgradeRecipe extends ComputerConvertRecipe {
 | 
			
		||||
    private final ComputerFamily family;
 | 
			
		||||
    private final Item result;
 | 
			
		||||
 | 
			
		||||
    private ComputerUpgradeRecipe(ResourceLocation identifier, ShapedRecipeSpec recipe, ComputerFamily family) {
 | 
			
		||||
    private ComputerUpgradeRecipe(ResourceLocation identifier, ShapedRecipeSpec recipe) {
 | 
			
		||||
        super(identifier, recipe);
 | 
			
		||||
        this.family = family;
 | 
			
		||||
        this.result = recipe.result().getItem();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static DataResult<ComputerUpgradeRecipe> of(ResourceLocation id, ShapedRecipeSpec recipe) {
 | 
			
		||||
        if (!(recipe.result().getItem() instanceof IComputerItem)) {
 | 
			
		||||
            return DataResult.error(() -> recipe.result().getItem() + " is not a computer item");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return DataResult.success(new ComputerUpgradeRecipe(id, recipe));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected ItemStack convert(IComputerItem item, ItemStack stack) {
 | 
			
		||||
        return item.withFamily(stack, family);
 | 
			
		||||
        return item.changeItem(stack, result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public RecipeSerializer<ComputerUpgradeRecipe> getSerializer() {
 | 
			
		||||
        return ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static class Serializer implements RecipeSerializer<ComputerUpgradeRecipe> {
 | 
			
		||||
        @Override
 | 
			
		||||
        public ComputerUpgradeRecipe fromJson(ResourceLocation identifier, JsonObject json) {
 | 
			
		||||
            var recipe = ShapedRecipeSpec.fromJson(json);
 | 
			
		||||
            var family = ComputerFamily.getFamily(json, "family");
 | 
			
		||||
            return new ComputerUpgradeRecipe(identifier, recipe, family);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public ComputerUpgradeRecipe fromNetwork(ResourceLocation identifier, FriendlyByteBuf buf) {
 | 
			
		||||
            var recipe = ShapedRecipeSpec.fromNetwork(buf);
 | 
			
		||||
            var family = buf.readEnum(ComputerFamily.class);
 | 
			
		||||
            return new ComputerUpgradeRecipe(identifier, recipe, family);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void toNetwork(FriendlyByteBuf buf, ComputerUpgradeRecipe recipe) {
 | 
			
		||||
            recipe.toSpec().toNetwork(buf);
 | 
			
		||||
            buf.writeEnum(recipe.family);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.network;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A type of message to send over the network.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Much like recipe or argument serialisers, each type of {@link NetworkMessage} should have a unique type associated
 | 
			
		||||
 * with it. This holds platform-specific information about how the packet should be sent over the network.
 | 
			
		||||
 *
 | 
			
		||||
 * @param <T> The type of message to send
 | 
			
		||||
 * @see NetworkMessages
 | 
			
		||||
 * @see NetworkMessage#type()
 | 
			
		||||
 */
 | 
			
		||||
public interface MessageType<T extends NetworkMessage<?>> {
 | 
			
		||||
}
 | 
			
		||||
@@ -17,6 +17,13 @@ import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
 * @see ServerNetworkContext
 | 
			
		||||
 */
 | 
			
		||||
public interface NetworkMessage<T> {
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the type of this message.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The type of this message.
 | 
			
		||||
     */
 | 
			
		||||
    MessageType<?> type();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Write this packet to a buffer.
 | 
			
		||||
     * <p>
 | 
			
		||||
@@ -24,7 +31,7 @@ public interface NetworkMessage<T> {
 | 
			
		||||
     *
 | 
			
		||||
     * @param buf The buffer to write data to.
 | 
			
		||||
     */
 | 
			
		||||
    void toBytes(FriendlyByteBuf buf);
 | 
			
		||||
    void write(FriendlyByteBuf buf);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handle this {@link NetworkMessage}.
 | 
			
		||||
 
 | 
			
		||||
@@ -4,48 +4,84 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.network;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.shared.network.client.*;
 | 
			
		||||
import dan200.computercraft.shared.network.server.*;
 | 
			
		||||
import dan200.computercraft.shared.platform.PlatformHelper;
 | 
			
		||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
 | 
			
		||||
import it.unimi.dsi.fastutil.ints.IntSet;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
 | 
			
		||||
import java.util.function.Function;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Registry for all packets provided by CC: Tweaked.
 | 
			
		||||
 * List of all {@link MessageType}s provided by CC: Tweaked.
 | 
			
		||||
 *
 | 
			
		||||
 * @see PlatformHelper The platform helper is used to send packets.
 | 
			
		||||
 */
 | 
			
		||||
public final class NetworkMessages {
 | 
			
		||||
    private static final IntSet seenIds = new IntOpenHashSet();
 | 
			
		||||
    private static final Set<String> seenChannel = new HashSet<>();
 | 
			
		||||
    private static final List<MessageType<? extends NetworkMessage<ServerNetworkContext>>> serverMessages = new ArrayList<>();
 | 
			
		||||
    private static final List<MessageType<? extends NetworkMessage<ClientNetworkContext>>> clientMessages = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
    public static final MessageType<ComputerActionServerMessage> COMPUTER_ACTION = registerServerbound(0, "computer_action", ComputerActionServerMessage.class, ComputerActionServerMessage::new);
 | 
			
		||||
    public static final MessageType<QueueEventServerMessage> QUEUE_EVENT = registerServerbound(1, "queue_event", QueueEventServerMessage.class, QueueEventServerMessage::new);
 | 
			
		||||
    public static final MessageType<KeyEventServerMessage> KEY_EVENT = registerServerbound(2, "key_event", KeyEventServerMessage.class, KeyEventServerMessage::new);
 | 
			
		||||
    public static final MessageType<MouseEventServerMessage> MOUSE_EVENT = registerServerbound(3, "mouse_event", MouseEventServerMessage.class, MouseEventServerMessage::new);
 | 
			
		||||
    public static final MessageType<UploadFileMessage> UPLOAD_FILE = registerServerbound(4, "upload_file", UploadFileMessage.class, UploadFileMessage::new);
 | 
			
		||||
 | 
			
		||||
    public static final MessageType<ChatTableClientMessage> CHAT_TABLE = registerClientbound(10, "chat_table", ChatTableClientMessage.class, ChatTableClientMessage::new);
 | 
			
		||||
    public static final MessageType<PocketComputerDataMessage> POCKET_COMPUTER_DATA = registerClientbound(11, "pocket_computer_data", PocketComputerDataMessage.class, PocketComputerDataMessage::new);
 | 
			
		||||
    public static final MessageType<PocketComputerDeletedClientMessage> POCKET_COMPUTER_DELETED = registerClientbound(12, "pocket_computer_deleted", PocketComputerDeletedClientMessage.class, PocketComputerDeletedClientMessage::new);
 | 
			
		||||
    public static final MessageType<ComputerTerminalClientMessage> COMPUTER_TERMINAL = registerClientbound(13, "computer_terminal", ComputerTerminalClientMessage.class, ComputerTerminalClientMessage::new);
 | 
			
		||||
    public static final MessageType<PlayRecordClientMessage> PLAY_RECORD = registerClientbound(14, "play_record", PlayRecordClientMessage.class, PlayRecordClientMessage::new);
 | 
			
		||||
    public static final MessageType<MonitorClientMessage> MONITOR_CLIENT = registerClientbound(15, "monitor_client", MonitorClientMessage.class, MonitorClientMessage::new);
 | 
			
		||||
    public static final MessageType<SpeakerAudioClientMessage> SPEAKER_AUDIO = registerClientbound(16, "speaker_audio", SpeakerAudioClientMessage.class, SpeakerAudioClientMessage::new);
 | 
			
		||||
    public static final MessageType<SpeakerMoveClientMessage> SPEAKER_MOVE = registerClientbound(17, "speaker_move", SpeakerMoveClientMessage.class, SpeakerMoveClientMessage::new);
 | 
			
		||||
    public static final MessageType<SpeakerPlayClientMessage> SPEAKER_PLAY = registerClientbound(18, "speaker_play", SpeakerPlayClientMessage.class, SpeakerPlayClientMessage::new);
 | 
			
		||||
    public static final MessageType<SpeakerStopClientMessage> SPEAKER_STOP = registerClientbound(19, "speaker_stop", SpeakerStopClientMessage.class, SpeakerStopClientMessage::new);
 | 
			
		||||
    public static final MessageType<UploadResultMessage> UPLOAD_RESULT = registerClientbound(20, "upload_result", UploadResultMessage.class, UploadResultMessage::new);
 | 
			
		||||
    public static final MessageType<UpgradesLoadedMessage> UPGRADES_LOADED = registerClientbound(21, "upgrades_loaded", UpgradesLoadedMessage.class, UpgradesLoadedMessage::new);
 | 
			
		||||
 | 
			
		||||
    private NetworkMessages() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public interface PacketRegistry {
 | 
			
		||||
        <T extends NetworkMessage<ClientNetworkContext>> void registerClientbound(int id, Class<T> type, Function<FriendlyByteBuf, T> decoder);
 | 
			
		||||
 | 
			
		||||
        <T extends NetworkMessage<ServerNetworkContext>> void registerServerbound(int id, Class<T> type, Function<FriendlyByteBuf, T> decoder);
 | 
			
		||||
    private static <C, T extends NetworkMessage<C>> MessageType<T> register(
 | 
			
		||||
        List<MessageType<? extends NetworkMessage<C>>> messages,
 | 
			
		||||
        int id, String channel, Class<T> klass, FriendlyByteBuf.Reader<T> reader
 | 
			
		||||
    ) {
 | 
			
		||||
        if (!seenIds.add(id)) throw new IllegalArgumentException("Duplicate id " + id);
 | 
			
		||||
        if (!seenChannel.add(channel)) throw new IllegalArgumentException("Duplicate channel " + channel);
 | 
			
		||||
        var type = PlatformHelper.get().createMessageType(id, new ResourceLocation(ComputerCraftAPI.MOD_ID, channel), klass, reader);
 | 
			
		||||
        messages.add(type);
 | 
			
		||||
        return type;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void register(PacketRegistry registry) {
 | 
			
		||||
        // Server messages
 | 
			
		||||
        registry.registerServerbound(0, ComputerActionServerMessage.class, ComputerActionServerMessage::new);
 | 
			
		||||
        registry.registerServerbound(1, QueueEventServerMessage.class, QueueEventServerMessage::new);
 | 
			
		||||
        registry.registerServerbound(2, KeyEventServerMessage.class, KeyEventServerMessage::new);
 | 
			
		||||
        registry.registerServerbound(3, MouseEventServerMessage.class, MouseEventServerMessage::new);
 | 
			
		||||
        registry.registerServerbound(4, UploadFileMessage.class, UploadFileMessage::new);
 | 
			
		||||
    private static <T extends NetworkMessage<ServerNetworkContext>> MessageType<T> registerServerbound(int id, String channel, Class<T> klass, FriendlyByteBuf.Reader<T> reader) {
 | 
			
		||||
        return register(serverMessages, id, channel, klass, reader);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        // Client messages
 | 
			
		||||
        registry.registerClientbound(10, ChatTableClientMessage.class, ChatTableClientMessage::new);
 | 
			
		||||
        registry.registerClientbound(11, PocketComputerDataMessage.class, PocketComputerDataMessage::new);
 | 
			
		||||
        registry.registerClientbound(12, PocketComputerDeletedClientMessage.class, PocketComputerDeletedClientMessage::new);
 | 
			
		||||
        registry.registerClientbound(13, ComputerTerminalClientMessage.class, ComputerTerminalClientMessage::new);
 | 
			
		||||
        registry.registerClientbound(14, PlayRecordClientMessage.class, PlayRecordClientMessage::new);
 | 
			
		||||
        registry.registerClientbound(15, MonitorClientMessage.class, MonitorClientMessage::new);
 | 
			
		||||
        registry.registerClientbound(16, SpeakerAudioClientMessage.class, SpeakerAudioClientMessage::new);
 | 
			
		||||
        registry.registerClientbound(17, SpeakerMoveClientMessage.class, SpeakerMoveClientMessage::new);
 | 
			
		||||
        registry.registerClientbound(18, SpeakerPlayClientMessage.class, SpeakerPlayClientMessage::new);
 | 
			
		||||
        registry.registerClientbound(19, SpeakerStopClientMessage.class, SpeakerStopClientMessage::new);
 | 
			
		||||
        registry.registerClientbound(20, UploadResultMessage.class, UploadResultMessage::new);
 | 
			
		||||
        registry.registerClientbound(21, UpgradesLoadedMessage.class, UpgradesLoadedMessage::new);
 | 
			
		||||
    private static <T extends NetworkMessage<ClientNetworkContext>> MessageType<T> registerClientbound(int id, String channel, Class<T> klass, FriendlyByteBuf.Reader<T> reader) {
 | 
			
		||||
        return register(clientMessages, id, channel, klass, reader);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all serverbound message types.
 | 
			
		||||
     *
 | 
			
		||||
     * @return An unmodifiable sequence of all serverbound message types.
 | 
			
		||||
     */
 | 
			
		||||
    public static Collection<MessageType<? extends NetworkMessage<ServerNetworkContext>>> getServerbound() {
 | 
			
		||||
        return Collections.unmodifiableCollection(serverMessages);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all clientbound message types.
 | 
			
		||||
     *
 | 
			
		||||
     * @return An unmodifiable sequence of all clientbound message types.
 | 
			
		||||
     */
 | 
			
		||||
    public static Collection<MessageType<? extends NetworkMessage<ClientNetworkContext>>> getClientbound() {
 | 
			
		||||
        return Collections.unmodifiableCollection(clientMessages);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,9 @@
 | 
			
		||||
package dan200.computercraft.shared.network.client;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.command.text.TableBuilder;
 | 
			
		||||
import dan200.computercraft.shared.network.MessageType;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessages;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
 | 
			
		||||
@@ -43,7 +45,7 @@ public class ChatTableClientMessage implements NetworkMessage<ClientNetworkConte
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void toBytes(FriendlyByteBuf buf) {
 | 
			
		||||
    public void write(FriendlyByteBuf buf) {
 | 
			
		||||
        buf.writeUtf(table.getId(), MAX_LEN);
 | 
			
		||||
        buf.writeVarInt(table.getColumns());
 | 
			
		||||
        buf.writeBoolean(table.getHeaders() != null);
 | 
			
		||||
@@ -63,4 +65,9 @@ public class ChatTableClientMessage implements NetworkMessage<ClientNetworkConte
 | 
			
		||||
    public void handle(ClientNetworkContext context) {
 | 
			
		||||
        context.handleChatTable(table);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public MessageType<ChatTableClientMessage> type() {
 | 
			
		||||
        return NetworkMessages.CHAT_TABLE;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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() {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,9 @@
 | 
			
		||||
package dan200.computercraft.shared.network.client;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.computer.terminal.TerminalState;
 | 
			
		||||
import dan200.computercraft.shared.network.MessageType;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessages;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.world.inventory.AbstractContainerMenu;
 | 
			
		||||
 | 
			
		||||
@@ -25,7 +27,7 @@ public class ComputerTerminalClientMessage implements NetworkMessage<ClientNetwo
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void toBytes(FriendlyByteBuf buf) {
 | 
			
		||||
    public void write(FriendlyByteBuf buf) {
 | 
			
		||||
        buf.writeVarInt(containerId);
 | 
			
		||||
        terminal.write(buf);
 | 
			
		||||
    }
 | 
			
		||||
@@ -34,4 +36,9 @@ public class ComputerTerminalClientMessage implements NetworkMessage<ClientNetwo
 | 
			
		||||
    public void handle(ClientNetworkContext context) {
 | 
			
		||||
        context.handleComputerTerminal(containerId, terminal);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public MessageType<ComputerTerminalClientMessage> type() {
 | 
			
		||||
        return NetworkMessages.COMPUTER_TERMINAL;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,9 @@
 | 
			
		||||
package dan200.computercraft.shared.network.client;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.computer.terminal.TerminalState;
 | 
			
		||||
import dan200.computercraft.shared.network.MessageType;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessages;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
 | 
			
		||||
@@ -25,7 +27,7 @@ public class MonitorClientMessage implements NetworkMessage<ClientNetworkContext
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void toBytes(FriendlyByteBuf buf) {
 | 
			
		||||
    public void write(FriendlyByteBuf buf) {
 | 
			
		||||
        buf.writeBlockPos(pos);
 | 
			
		||||
        state.write(buf);
 | 
			
		||||
    }
 | 
			
		||||
@@ -34,4 +36,9 @@ public class MonitorClientMessage implements NetworkMessage<ClientNetworkContext
 | 
			
		||||
    public void handle(ClientNetworkContext context) {
 | 
			
		||||
        context.handleMonitorData(pos, state);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public MessageType<MonitorClientMessage> type() {
 | 
			
		||||
        return NetworkMessages.MONITOR_CLIENT;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,9 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.network.client;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.network.MessageType;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessages;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveBlockEntity;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
@@ -43,7 +45,7 @@ public class PlayRecordClientMessage implements NetworkMessage<ClientNetworkCont
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void toBytes(FriendlyByteBuf buf) {
 | 
			
		||||
    public void write(FriendlyByteBuf buf) {
 | 
			
		||||
        buf.writeBlockPos(pos);
 | 
			
		||||
        buf.writeNullable(soundEvent, (b, e) -> e.writeToNetwork(b));
 | 
			
		||||
        buf.writeNullable(name, FriendlyByteBuf::writeUtf);
 | 
			
		||||
@@ -53,4 +55,9 @@ public class PlayRecordClientMessage implements NetworkMessage<ClientNetworkCont
 | 
			
		||||
    public void handle(ClientNetworkContext context) {
 | 
			
		||||
        context.handlePlayRecord(pos, soundEvent, name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public MessageType<PlayRecordClientMessage> type() {
 | 
			
		||||
        return NetworkMessages.PLAY_RECORD;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,9 @@ package dan200.computercraft.shared.network.client;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerState;
 | 
			
		||||
import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
 | 
			
		||||
import dan200.computercraft.shared.computer.terminal.TerminalState;
 | 
			
		||||
import dan200.computercraft.shared.network.MessageType;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessages;
 | 
			
		||||
import dan200.computercraft.shared.pocket.core.PocketServerComputer;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
 | 
			
		||||
@@ -35,7 +37,7 @@ public class PocketComputerDataMessage implements NetworkMessage<ClientNetworkCo
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void toBytes(FriendlyByteBuf buf) {
 | 
			
		||||
    public void write(FriendlyByteBuf buf) {
 | 
			
		||||
        buf.writeVarInt(instanceId);
 | 
			
		||||
        buf.writeEnum(state);
 | 
			
		||||
        buf.writeVarInt(lightState);
 | 
			
		||||
@@ -46,4 +48,9 @@ public class PocketComputerDataMessage implements NetworkMessage<ClientNetworkCo
 | 
			
		||||
    public void handle(ClientNetworkContext context) {
 | 
			
		||||
        context.handlePocketComputerData(instanceId, state, lightState, terminal);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public MessageType<PocketComputerDataMessage> type() {
 | 
			
		||||
        return NetworkMessages.POCKET_COMPUTER_DATA;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,9 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.network.client;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.network.MessageType;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessages;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -20,7 +22,7 @@ public class PocketComputerDeletedClientMessage implements NetworkMessage<Client
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void toBytes(FriendlyByteBuf buf) {
 | 
			
		||||
    public void write(FriendlyByteBuf buf) {
 | 
			
		||||
        buf.writeVarInt(instanceId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -28,4 +30,9 @@ public class PocketComputerDeletedClientMessage implements NetworkMessage<Client
 | 
			
		||||
    public void handle(ClientNetworkContext context) {
 | 
			
		||||
        context.handlePocketComputerDeleted(instanceId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public MessageType<PocketComputerDeletedClientMessage> type() {
 | 
			
		||||
        return NetworkMessages.POCKET_COMPUTER_DELETED;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,17 +4,16 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.network.client;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.network.MessageType;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessages;
 | 
			
		||||
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>
 | 
			
		||||
@@ -25,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) {
 | 
			
		||||
@@ -40,22 +39,26 @@ 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
 | 
			
		||||
    public void toBytes(FriendlyByteBuf buf) {
 | 
			
		||||
    public void write(FriendlyByteBuf buf) {
 | 
			
		||||
        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
 | 
			
		||||
    public MessageType<SpeakerAudioClientMessage> type() {
 | 
			
		||||
        return NetworkMessages.SPEAKER_AUDIO;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,9 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.network.client;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.network.MessageType;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessages;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerBlockEntity;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
@@ -33,7 +35,7 @@ public class SpeakerMoveClientMessage implements NetworkMessage<ClientNetworkCon
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void toBytes(FriendlyByteBuf buf) {
 | 
			
		||||
    public void write(FriendlyByteBuf buf) {
 | 
			
		||||
        buf.writeUUID(source);
 | 
			
		||||
        pos.write(buf);
 | 
			
		||||
    }
 | 
			
		||||
@@ -42,4 +44,9 @@ public class SpeakerMoveClientMessage implements NetworkMessage<ClientNetworkCon
 | 
			
		||||
    public void handle(ClientNetworkContext context) {
 | 
			
		||||
        context.handleSpeakerMove(source, pos);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public MessageType<SpeakerMoveClientMessage> type() {
 | 
			
		||||
        return NetworkMessages.SPEAKER_MOVE;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,9 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.network.client;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.network.MessageType;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessages;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerBlockEntity;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
@@ -43,7 +45,7 @@ public class SpeakerPlayClientMessage implements NetworkMessage<ClientNetworkCon
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void toBytes(FriendlyByteBuf buf) {
 | 
			
		||||
    public void write(FriendlyByteBuf buf) {
 | 
			
		||||
        buf.writeUUID(source);
 | 
			
		||||
        pos.write(buf);
 | 
			
		||||
        buf.writeResourceLocation(sound);
 | 
			
		||||
@@ -55,4 +57,9 @@ public class SpeakerPlayClientMessage implements NetworkMessage<ClientNetworkCon
 | 
			
		||||
    public void handle(ClientNetworkContext context) {
 | 
			
		||||
        context.handleSpeakerPlay(source, pos, sound, volume, pitch);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public MessageType<SpeakerPlayClientMessage> type() {
 | 
			
		||||
        return NetworkMessages.SPEAKER_PLAY;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,9 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.network.client;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.network.MessageType;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessages;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerBlockEntity;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
 | 
			
		||||
@@ -29,7 +31,7 @@ public class SpeakerStopClientMessage implements NetworkMessage<ClientNetworkCon
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void toBytes(FriendlyByteBuf buf) {
 | 
			
		||||
    public void write(FriendlyByteBuf buf) {
 | 
			
		||||
        buf.writeUUID(source);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -37,4 +39,9 @@ public class SpeakerStopClientMessage implements NetworkMessage<ClientNetworkCon
 | 
			
		||||
    public void handle(ClientNetworkContext context) {
 | 
			
		||||
        context.handleSpeakerStop(source);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public MessageType<SpeakerStopClientMessage> type() {
 | 
			
		||||
        return NetworkMessages.SPEAKER_STOP;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,9 @@ import dan200.computercraft.api.upgrades.UpgradeSerialiser;
 | 
			
		||||
import dan200.computercraft.impl.PocketUpgrades;
 | 
			
		||||
import dan200.computercraft.impl.TurtleUpgrades;
 | 
			
		||||
import dan200.computercraft.impl.UpgradeManager;
 | 
			
		||||
import dan200.computercraft.shared.network.MessageType;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessages;
 | 
			
		||||
import dan200.computercraft.shared.platform.PlatformHelper;
 | 
			
		||||
import net.minecraft.core.Registry;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
@@ -27,7 +29,7 @@ import java.util.Objects;
 | 
			
		||||
/**
 | 
			
		||||
 * Syncs turtle and pocket upgrades to the client.
 | 
			
		||||
 */
 | 
			
		||||
public class UpgradesLoadedMessage implements NetworkMessage<ClientNetworkContext> {
 | 
			
		||||
public final class UpgradesLoadedMessage implements NetworkMessage<ClientNetworkContext> {
 | 
			
		||||
    private final Map<String, UpgradeManager.UpgradeWrapper<TurtleUpgradeSerialiser<?>, ITurtleUpgrade>> turtleUpgrades;
 | 
			
		||||
    private final Map<String, UpgradeManager.UpgradeWrapper<PocketUpgradeSerialiser<?>, IPocketUpgrade>> pocketUpgrades;
 | 
			
		||||
 | 
			
		||||
@@ -65,7 +67,7 @@ public class UpgradesLoadedMessage implements NetworkMessage<ClientNetworkContex
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void toBytes(FriendlyByteBuf buf) {
 | 
			
		||||
    public void write(FriendlyByteBuf buf) {
 | 
			
		||||
        toBytes(buf, TurtleUpgradeSerialiser.registryId(), turtleUpgrades);
 | 
			
		||||
        toBytes(buf, PocketUpgradeSerialiser.registryId(), pocketUpgrades);
 | 
			
		||||
    }
 | 
			
		||||
@@ -95,4 +97,9 @@ public class UpgradesLoadedMessage implements NetworkMessage<ClientNetworkContex
 | 
			
		||||
        TurtleUpgrades.instance().loadFromNetwork(turtleUpgrades);
 | 
			
		||||
        PocketUpgrades.instance().loadFromNetwork(pocketUpgrades);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public MessageType<UpgradesLoadedMessage> type() {
 | 
			
		||||
        return NetworkMessages.UPGRADES_LOADED;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,9 @@ package dan200.computercraft.shared.network.client;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.core.util.Nullability;
 | 
			
		||||
import dan200.computercraft.shared.computer.upload.UploadResult;
 | 
			
		||||
import dan200.computercraft.shared.network.MessageType;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessages;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.world.inventory.AbstractContainerMenu;
 | 
			
		||||
@@ -43,7 +45,7 @@ public class UploadResultMessage implements NetworkMessage<ClientNetworkContext>
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void toBytes(FriendlyByteBuf buf) {
 | 
			
		||||
    public void write(FriendlyByteBuf buf) {
 | 
			
		||||
        buf.writeVarInt(containerId);
 | 
			
		||||
        buf.writeEnum(result);
 | 
			
		||||
        if (result == UploadResult.ERROR) buf.writeComponent(Nullability.assertNonNull(errorMessage));
 | 
			
		||||
@@ -53,4 +55,9 @@ public class UploadResultMessage implements NetworkMessage<ClientNetworkContext>
 | 
			
		||||
    public void handle(ClientNetworkContext context) {
 | 
			
		||||
        context.handleUploadResult(containerId, result, errorMessage);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public MessageType<UploadResultMessage> type() {
 | 
			
		||||
        return NetworkMessages.UPLOAD_RESULT;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,8 @@
 | 
			
		||||
package dan200.computercraft.shared.network.server;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.computer.menu.ComputerMenu;
 | 
			
		||||
import dan200.computercraft.shared.network.MessageType;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessages;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.world.inventory.AbstractContainerMenu;
 | 
			
		||||
 | 
			
		||||
@@ -23,8 +25,8 @@ public class ComputerActionServerMessage extends ComputerServerMessage {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void toBytes(FriendlyByteBuf buf) {
 | 
			
		||||
        super.toBytes(buf);
 | 
			
		||||
    public void write(FriendlyByteBuf buf) {
 | 
			
		||||
        super.write(buf);
 | 
			
		||||
        buf.writeEnum(action);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -37,6 +39,11 @@ public class ComputerActionServerMessage extends ComputerServerMessage {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public MessageType<ComputerActionServerMessage> type() {
 | 
			
		||||
        return NetworkMessages.COMPUTER_ACTION;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public enum Action {
 | 
			
		||||
        TURN_ON,
 | 
			
		||||
        SHUTDOWN,
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ public abstract class ComputerServerMessage implements NetworkMessage<ServerNetw
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    @OverridingMethodsMustInvokeSuper
 | 
			
		||||
    public void toBytes(FriendlyByteBuf buf) {
 | 
			
		||||
    public void write(FriendlyByteBuf buf) {
 | 
			
		||||
        buf.writeVarInt(containerId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,8 @@
 | 
			
		||||
package dan200.computercraft.shared.network.server;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.computer.menu.ComputerMenu;
 | 
			
		||||
import dan200.computercraft.shared.network.MessageType;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessages;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.world.inventory.AbstractContainerMenu;
 | 
			
		||||
 | 
			
		||||
@@ -30,8 +32,8 @@ public class KeyEventServerMessage extends ComputerServerMessage {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void toBytes(FriendlyByteBuf buf) {
 | 
			
		||||
        super.toBytes(buf);
 | 
			
		||||
    public void write(FriendlyByteBuf buf) {
 | 
			
		||||
        super.write(buf);
 | 
			
		||||
        buf.writeByte(type);
 | 
			
		||||
        buf.writeVarInt(key);
 | 
			
		||||
    }
 | 
			
		||||
@@ -45,4 +47,9 @@ public class KeyEventServerMessage extends ComputerServerMessage {
 | 
			
		||||
            input.keyDown(key, type == TYPE_REPEAT);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public MessageType<KeyEventServerMessage> type() {
 | 
			
		||||
        return NetworkMessages.KEY_EVENT;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,8 @@
 | 
			
		||||
package dan200.computercraft.shared.network.server;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.computer.menu.ComputerMenu;
 | 
			
		||||
import dan200.computercraft.shared.network.MessageType;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessages;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.world.inventory.AbstractContainerMenu;
 | 
			
		||||
 | 
			
		||||
@@ -37,8 +39,8 @@ public class MouseEventServerMessage extends ComputerServerMessage {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void toBytes(FriendlyByteBuf buf) {
 | 
			
		||||
        super.toBytes(buf);
 | 
			
		||||
    public void write(FriendlyByteBuf buf) {
 | 
			
		||||
        super.write(buf);
 | 
			
		||||
        buf.writeByte(type);
 | 
			
		||||
        buf.writeVarInt(arg);
 | 
			
		||||
        buf.writeVarInt(x);
 | 
			
		||||
@@ -55,4 +57,9 @@ public class MouseEventServerMessage extends ComputerServerMessage {
 | 
			
		||||
            case TYPE_SCROLL -> input.mouseScroll(arg, x, y);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public MessageType<MouseEventServerMessage> type() {
 | 
			
		||||
        return NetworkMessages.MOUSE_EVENT;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,8 @@ package dan200.computercraft.shared.network.server;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ServerComputer;
 | 
			
		||||
import dan200.computercraft.shared.computer.menu.ComputerMenu;
 | 
			
		||||
import dan200.computercraft.shared.computer.menu.ServerInputHandler;
 | 
			
		||||
import dan200.computercraft.shared.network.MessageType;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessages;
 | 
			
		||||
import dan200.computercraft.shared.util.NBTUtil;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.world.inventory.AbstractContainerMenu;
 | 
			
		||||
@@ -37,8 +39,8 @@ public class QueueEventServerMessage extends ComputerServerMessage {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void toBytes(FriendlyByteBuf buf) {
 | 
			
		||||
        super.toBytes(buf);
 | 
			
		||||
    public void write(FriendlyByteBuf buf) {
 | 
			
		||||
        super.write(buf);
 | 
			
		||||
        buf.writeUtf(event);
 | 
			
		||||
        buf.writeNbt(args == null ? null : NBTUtil.encodeObjects(args));
 | 
			
		||||
    }
 | 
			
		||||
@@ -47,4 +49,9 @@ public class QueueEventServerMessage extends ComputerServerMessage {
 | 
			
		||||
    protected void handle(ServerNetworkContext context, ComputerMenu container) {
 | 
			
		||||
        container.getInput().queueEvent(event, args);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public MessageType<QueueEventServerMessage> type() {
 | 
			
		||||
        return NetworkMessages.QUEUE_EVENT;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -9,6 +9,8 @@ import dan200.computercraft.shared.computer.menu.ComputerMenu;
 | 
			
		||||
import dan200.computercraft.shared.computer.upload.FileSlice;
 | 
			
		||||
import dan200.computercraft.shared.computer.upload.FileUpload;
 | 
			
		||||
import dan200.computercraft.shared.config.Config;
 | 
			
		||||
import dan200.computercraft.shared.network.MessageType;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessages;
 | 
			
		||||
import io.netty.handler.codec.DecoderException;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.world.inventory.AbstractContainerMenu;
 | 
			
		||||
@@ -91,8 +93,8 @@ public class UploadFileMessage extends ComputerServerMessage {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void toBytes(FriendlyByteBuf buf) {
 | 
			
		||||
        super.toBytes(buf);
 | 
			
		||||
    public void write(FriendlyByteBuf buf) {
 | 
			
		||||
        super.write(buf);
 | 
			
		||||
        buf.writeUUID(uuid);
 | 
			
		||||
        buf.writeByte(flag);
 | 
			
		||||
 | 
			
		||||
@@ -166,4 +168,9 @@ public class UploadFileMessage extends ComputerServerMessage {
 | 
			
		||||
        input.continueUpload(uuid, slices);
 | 
			
		||||
        if ((flag & FLAG_LAST) != 0) input.finishUpload(player, uuid);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public MessageType<UploadFileMessage> type() {
 | 
			
		||||
        return NetworkMessages.UPLOAD_FILE;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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,38 +153,46 @@ 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()) {
 | 
			
		||||
                for (var computer : computers.entrySet()) unmountDisk(computer.getKey(), computer.getValue());
 | 
			
		||||
        // Unmount old disk
 | 
			
		||||
        if (!media.stack().isEmpty()) {
 | 
			
		||||
            for (var computer : computers.entrySet()) unmountDisk(computer.getKey(), computer.getValue());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Stop music
 | 
			
		||||
        if (recordPlaying) {
 | 
			
		||||
            stopRecord();
 | 
			
		||||
            recordPlaying = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Use our new media, and (if needed) mount the new disk.
 | 
			
		||||
        mount = null;
 | 
			
		||||
        media = newMedia;
 | 
			
		||||
        stackDirty.set(false);
 | 
			
		||||
 | 
			
		||||
        if (!newStack.isEmpty() && !computers.isEmpty()) {
 | 
			
		||||
            var mount = getOrCreateMount(true);
 | 
			
		||||
            for (var entry : computers.entrySet()) {
 | 
			
		||||
                mountDisk(entry.getKey(), entry.getValue(), mount);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Stop music
 | 
			
		||||
            if (recordPlaying) {
 | 
			
		||||
                stopRecord();
 | 
			
		||||
                recordPlaying = false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            mount = null;
 | 
			
		||||
            this.media = media;
 | 
			
		||||
 | 
			
		||||
            mountAll();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -166,7 +200,7 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
 | 
			
		||||
        return getItem(0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    MediaStack getMedia() {
 | 
			
		||||
    synchronized MediaStack getMedia() {
 | 
			
		||||
        return media;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -181,16 +215,31 @@ 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);
 | 
			
		||||
            super.setChanged();
 | 
			
		||||
    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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -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,53 +285,50 @@ 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);
 | 
			
		||||
 | 
			
		||||
        if (mount == null) return;
 | 
			
		||||
        return mount;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        for (var entry : computers.entrySet()) {
 | 
			
		||||
            var computer = entry.getKey();
 | 
			
		||||
            var info = entry.getValue();
 | 
			
		||||
            if (info.mountPath != null) continue;
 | 
			
		||||
 | 
			
		||||
            if (mount instanceof WritableMount writable) {
 | 
			
		||||
                // Try mounting at the lowest numbered "disk" name we can
 | 
			
		||||
                var n = 1;
 | 
			
		||||
                while (info.mountPath == null) {
 | 
			
		||||
                    info.mountPath = computer.mountWritable(n == 1 ? "disk" : "disk" + n, writable);
 | 
			
		||||
                    n++;
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                // 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++;
 | 
			
		||||
                }
 | 
			
		||||
    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;
 | 
			
		||||
            while (info.mountPath == null) {
 | 
			
		||||
                info.mountPath = computer.mountWritable(n == 1 ? "disk" : "disk" + n, writable);
 | 
			
		||||
                n++;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            computer.queueEvent("disk", computer.getAttachmentName());
 | 
			
		||||
        } 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) {
 | 
			
		||||
@@ -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();
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user