mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-11-03 23:22:59 +00:00 
			
		
		
		
	Compare commits
	
		
			25 Commits
		
	
	
		
			v1.20.1-1.
			...
			v1.20.1-1.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					3ebdf7ef5e | ||
| 
						 | 
					905d4cb091 | ||
| 
						 | 
					e7ab05d064 | ||
| 
						 | 
					6ec34b42e5 | ||
| 
						 | 
					ab785a0906 | ||
| 
						 | 
					4541decd40 | ||
| 
						 | 
					747a5a53b4 | ||
| 
						 | 
					c0643fadca | ||
| 
						 | 
					0a31de43c2 | ||
| 
						 | 
					96b6947ef2 | ||
| 
						 | 
					e7a1065bfc | ||
| 
						 | 
					663eecff0c | ||
| 
						 | 
					e6125bcf60 | ||
| 
						 | 
					0d6c6e7ae7 | ||
| 
						 | 
					ae71eb3cae | ||
| 
						 | 
					3188197447 | ||
| 
						 | 
					6c8b391dab | ||
| 
						 | 
					b1248e4901 | ||
| 
						 | 
					56d97630e8 | ||
| 
						 | 
					e660192f08 | ||
| 
						 | 
					4e82bd352d | ||
| 
						 | 
					07113c3e9b | ||
| 
						 | 
					d562a051c7 | ||
| 
						 | 
					6ac09742fc | ||
| 
						 | 
					5dd6b9a637 | 
@@ -44,7 +44,7 @@ repos:
 | 
			
		||||
    name: Check Java codestyle
 | 
			
		||||
    files: ".*\\.java$"
 | 
			
		||||
    language: system
 | 
			
		||||
    entry: ./gradlew checkstyleMain checkstyleTest
 | 
			
		||||
    entry: ./gradlew checkstyle
 | 
			
		||||
    pass_filenames: false
 | 
			
		||||
    require_serial: true
 | 
			
		||||
  - id: illuaminate
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								.reuse/dep5
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								.reuse/dep5
									
									
									
									
									
								
							@@ -10,8 +10,8 @@ Files:
 | 
			
		||||
  projects/common/src/testMod/resources/data/cctest/structures/*
 | 
			
		||||
  projects/fabric/src/generated/*
 | 
			
		||||
  projects/forge/src/generated/*
 | 
			
		||||
  projects/web/src/export/index.json
 | 
			
		||||
  projects/web/src/export/items/minecraft/*
 | 
			
		||||
  projects/web/src/htmlTransform/export/index.json
 | 
			
		||||
  projects/web/src/htmlTransform/export/items/minecraft/*
 | 
			
		||||
Comment: Generated/data files are CC0.
 | 
			
		||||
Copyright: The CC: Tweaked Developers
 | 
			
		||||
License: CC0-1.0
 | 
			
		||||
@@ -37,10 +37,10 @@ Files:
 | 
			
		||||
  projects/fabric/src/testMod/resources/computercraft-gametest.fabric.mixins.json
 | 
			
		||||
  projects/fabric/src/testMod/resources/fabric.mod.json
 | 
			
		||||
  projects/forge/src/client/resources/computercraft-client.forge.mixins.json
 | 
			
		||||
  projects/web/src/mount/.settings
 | 
			
		||||
  projects/web/src/mount/example.nfp
 | 
			
		||||
  projects/web/src/mount/example.nft
 | 
			
		||||
  projects/web/src/mount/expr_template.lua
 | 
			
		||||
  projects/web/src/frontend/mount/.settings
 | 
			
		||||
  projects/web/src/frontend/mount/example.nfp
 | 
			
		||||
  projects/web/src/frontend/mount/example.nft
 | 
			
		||||
  projects/web/src/frontend/mount/expr_template.lua
 | 
			
		||||
  projects/web/tsconfig.json
 | 
			
		||||
Comment: Several assets where it's inconvenient to create a .license file.
 | 
			
		||||
Copyright: The CC: Tweaked Developers
 | 
			
		||||
@@ -56,7 +56,7 @@ Files:
 | 
			
		||||
  projects/core/src/main/resources/data/computercraft/lua/rom/autorun/.ignoreme
 | 
			
		||||
  projects/core/src/main/resources/data/computercraft/lua/rom/help/*
 | 
			
		||||
  projects/core/src/main/resources/data/computercraft/lua/rom/programs/fun/advanced/levels/*
 | 
			
		||||
  projects/web/src/export/items/computercraft/*
 | 
			
		||||
  projects/web/src/htmlTransform/export/items/computercraft/*
 | 
			
		||||
Comment: Bulk-license original assets as CCPL.
 | 
			
		||||
Copyright: 2011 Daniel Ratcliffe
 | 
			
		||||
License: LicenseRef-CCPL
 | 
			
		||||
 
 | 
			
		||||
@@ -100,7 +100,6 @@ about how you can build on that until you've covered everything!
 | 
			
		||||
[new-issue]: https://github.com/cc-tweaked/CC-Tweaked/issues/new/choose "Create a new issue"
 | 
			
		||||
[community]: README.md#community "Get in touch with the community."
 | 
			
		||||
[Adoptium]: https://adoptium.net/temurin/releases?version=17 "Download OpenJDK 17"
 | 
			
		||||
[checkstyle]: https://checkstyle.org/
 | 
			
		||||
[illuaminate]: https://github.com/SquidDev/illuaminate/ "Illuaminate on GitHub"
 | 
			
		||||
[weblate]: https://i18n.tweaked.cc/projects/cc-tweaked/minecraft/ "CC: Tweaked weblate instance"
 | 
			
		||||
[docs]: https://tweaked.cc/ "CC: Tweaked documentation"
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,7 @@ dependencies {
 | 
			
		||||
    implementation(libs.forgeGradle)
 | 
			
		||||
    implementation(libs.librarian)
 | 
			
		||||
    implementation(libs.minotaur)
 | 
			
		||||
    implementation(libs.quiltflower)
 | 
			
		||||
    implementation(libs.vineflower)
 | 
			
		||||
    implementation(libs.vanillaGradle)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ import cc.tweaked.gradle.MinecraftConfigurations
 | 
			
		||||
plugins {
 | 
			
		||||
    `java-library`
 | 
			
		||||
    id("fabric-loom")
 | 
			
		||||
    id("io.github.juuxel.loom-quiltflower")
 | 
			
		||||
    id("io.github.juuxel.loom-vineflower")
 | 
			
		||||
    id("cc-tweaked.java-convention")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -66,6 +66,7 @@ repositories {
 | 
			
		||||
            includeGroup("me.shedaniel.cloth")
 | 
			
		||||
            includeGroup("me.shedaniel")
 | 
			
		||||
            includeGroup("mezz.jei")
 | 
			
		||||
            includeGroup("org.teavm")
 | 
			
		||||
            includeModule("com.terraformersmc", "modmenu")
 | 
			
		||||
            includeModule("me.lucko", "fabric-permissions-api")
 | 
			
		||||
        }
 | 
			
		||||
@@ -99,7 +100,10 @@ sourceSets.all {
 | 
			
		||||
            check("FutureReturnValueIgnored", CheckSeverity.OFF) // Too many false positives with Netty
 | 
			
		||||
 | 
			
		||||
            check("NullAway", CheckSeverity.ERROR)
 | 
			
		||||
            option("NullAway:AnnotatedPackages", listOf("dan200.computercraft", "net.fabricmc.fabric.api").joinToString(","))
 | 
			
		||||
            option(
 | 
			
		||||
                "NullAway:AnnotatedPackages",
 | 
			
		||||
                listOf("dan200.computercraft", "cc.tweaked", "net.fabricmc.fabric.api").joinToString(","),
 | 
			
		||||
            )
 | 
			
		||||
            option("NullAway:ExcludedFieldAnnotations", listOf("org.spongepowered.asm.mixin.Shadow").joinToString(","))
 | 
			
		||||
            option("NullAway:CastToNonNullMethod", "dan200.computercraft.core.util.Nullability.assertNonNull")
 | 
			
		||||
            option("NullAway:CheckOptionalEmptiness")
 | 
			
		||||
@@ -174,6 +178,12 @@ project.plugins.withType(CCTweakedPlugin::class.java) {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tasks.register("checkstyle") {
 | 
			
		||||
    description = "Run Checkstyle on all sources"
 | 
			
		||||
    group = LifecycleBasePlugin.VERIFICATION_GROUP
 | 
			
		||||
    dependsOn(tasks.withType(Checkstyle::class.java))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
spotless {
 | 
			
		||||
    encoding = StandardCharsets.UTF_8
 | 
			
		||||
    lineEndings = LineEnding.UNIX
 | 
			
		||||
@@ -191,6 +201,7 @@ spotless {
 | 
			
		||||
 | 
			
		||||
    val ktlintConfig = mapOf(
 | 
			
		||||
        "ktlint_standard_no-wildcard-imports" to "disabled",
 | 
			
		||||
        "ktlint_standard_class-naming" to "disabled",
 | 
			
		||||
        "ij_kotlin_allow_trailing_comma" to "true",
 | 
			
		||||
        "ij_kotlin_allow_trailing_comma_on_call_site" to "true",
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
 | 
			
		||||
package cc.tweaked.gradle
 | 
			
		||||
 | 
			
		||||
import org.gradle.api.file.DirectoryProperty
 | 
			
		||||
import org.gradle.api.provider.Property
 | 
			
		||||
import org.gradle.api.tasks.AbstractExecTask
 | 
			
		||||
import org.gradle.api.tasks.OutputDirectory
 | 
			
		||||
@@ -11,5 +12,5 @@ import java.io.File
 | 
			
		||||
 | 
			
		||||
abstract class ExecToDir : AbstractExecTask<ExecToDir>(ExecToDir::class.java) {
 | 
			
		||||
    @get:OutputDirectory
 | 
			
		||||
    abstract val output: Property<File>
 | 
			
		||||
    abstract val output: DirectoryProperty
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,8 @@
 | 
			
		||||
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
 | 
			
		||||
@@ -124,3 +126,6 @@ class CloseScope : AutoCloseable {
 | 
			
		||||
 | 
			
		||||
/** Proxy method to avoid overload ambiguity. */
 | 
			
		||||
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
 | 
			
		||||
 
 | 
			
		||||
@@ -16,4 +16,10 @@ SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
    <!-- The commands API is documented in Lua. -->
 | 
			
		||||
    <suppress checks="SummaryJavadocCheck" files=".*[\\/]CommandAPI.java" />
 | 
			
		||||
 | 
			
		||||
    <!-- Allow putting files in other packages if they look like our TeaVM stubs. -->
 | 
			
		||||
    <suppress checks="PackageName" files=".*[\\/]T[A-Za-z]+.java" />
 | 
			
		||||
 | 
			
		||||
    <!-- Allow underscores in our test classes. -->
 | 
			
		||||
    <suppress checks="MethodName" files=".*Contract.java" />
 | 
			
		||||
</suppressions>
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ for _, file in ipairs(files.getFiles()) do
 | 
			
		||||
  local size = file.seek("end")
 | 
			
		||||
  file.seek("set", 0)
 | 
			
		||||
 | 
			
		||||
  print(file.getName() .. " " .. file.getSize())
 | 
			
		||||
  print(file.getName() .. " " .. size)
 | 
			
		||||
end
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ kotlin.jvm.target.validation.mode=error
 | 
			
		||||
 | 
			
		||||
# Mod properties
 | 
			
		||||
isUnstable=false
 | 
			
		||||
modVersion=1.108.0
 | 
			
		||||
modVersion=1.108.3
 | 
			
		||||
 | 
			
		||||
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
 | 
			
		||||
mcVersion=1.20.1
 | 
			
		||||
 
 | 
			
		||||
@@ -12,12 +12,12 @@ fabric-loader = "0.14.21"
 | 
			
		||||
forge = "47.1.0"
 | 
			
		||||
forgeSpi = "6.0.0"
 | 
			
		||||
mixin = "0.8.5"
 | 
			
		||||
parchment = "2023.06.26"
 | 
			
		||||
parchmentMc = "1.19.4"
 | 
			
		||||
parchment = "2023.08.20"
 | 
			
		||||
parchmentMc = "1.20.1"
 | 
			
		||||
 | 
			
		||||
# Normal dependencies
 | 
			
		||||
asm = "9.3"
 | 
			
		||||
autoService = "1.0.1"
 | 
			
		||||
asm = "9.5"
 | 
			
		||||
autoService = "1.1.1"
 | 
			
		||||
checkerFramework = "3.32.0"
 | 
			
		||||
cobalt = "0.7.3"
 | 
			
		||||
cobalt-next = "0.7.4" # Not a real version, used to constrain the version we accept.
 | 
			
		||||
@@ -29,8 +29,8 @@ jzlib = "1.1.3"
 | 
			
		||||
kotlin = "1.8.10"
 | 
			
		||||
kotlin-coroutines = "1.6.4"
 | 
			
		||||
netty = "4.1.82.Final"
 | 
			
		||||
nightConfig = "3.6.5"
 | 
			
		||||
slf4j = "1.7.36"
 | 
			
		||||
nightConfig = "3.6.7"
 | 
			
		||||
slf4j = "2.0.1"
 | 
			
		||||
 | 
			
		||||
# Minecraft mods
 | 
			
		||||
emi = "1.0.8+1.20.1"
 | 
			
		||||
@@ -45,34 +45,36 @@ rubidium = "0.6.1"
 | 
			
		||||
sodium = "mc1.20-0.4.10"
 | 
			
		||||
 | 
			
		||||
# Testing
 | 
			
		||||
byteBuddy = "1.14.2"
 | 
			
		||||
byteBuddy = "1.14.7"
 | 
			
		||||
hamcrest = "2.2"
 | 
			
		||||
jqwik = "1.7.2"
 | 
			
		||||
junit = "5.9.2"
 | 
			
		||||
jqwik = "1.7.4"
 | 
			
		||||
junit = "5.10.0"
 | 
			
		||||
 | 
			
		||||
# Build tools
 | 
			
		||||
cctJavadoc = "1.8.0"
 | 
			
		||||
checkstyle = "10.3.4"
 | 
			
		||||
checkstyle = "10.12.3"
 | 
			
		||||
curseForgeGradle = "1.0.14"
 | 
			
		||||
errorProne-core = "2.18.0"
 | 
			
		||||
errorProne-plugin = "3.0.1"
 | 
			
		||||
errorProne-core = "2.21.1"
 | 
			
		||||
errorProne-plugin = "3.1.0"
 | 
			
		||||
fabric-loom = "1.3.7"
 | 
			
		||||
forgeGradle = "6.0.8"
 | 
			
		||||
githubRelease = "2.2.12"
 | 
			
		||||
ideaExt = "1.1.6"
 | 
			
		||||
illuaminate = "0.1.0-40-g975cbc3"
 | 
			
		||||
githubRelease = "2.4.1"
 | 
			
		||||
ideaExt = "1.1.7"
 | 
			
		||||
illuaminate = "0.1.0-44-g9ee0055"
 | 
			
		||||
librarian = "1.+"
 | 
			
		||||
minotaur = "2.+"
 | 
			
		||||
mixinGradle = "0.7.+"
 | 
			
		||||
nullAway = "0.9.9"
 | 
			
		||||
quiltflower = "1.10.0"
 | 
			
		||||
spotless = "6.17.0"
 | 
			
		||||
spotless = "6.21.0"
 | 
			
		||||
taskTree = "2.1.1"
 | 
			
		||||
vanillaGradle = "0.2.1-SNAPSHOT"
 | 
			
		||||
vineflower = "1.11.0"
 | 
			
		||||
teavm = "0.9.0-SQUID.1"
 | 
			
		||||
 | 
			
		||||
[libraries]
 | 
			
		||||
# Normal dependencies
 | 
			
		||||
asm = { module = "org.ow2.asm:asm", version.ref = "asm" }
 | 
			
		||||
asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" }
 | 
			
		||||
autoService = { module = "com.google.auto.service:auto-service", version.ref = "autoService" }
 | 
			
		||||
checkerFramework = { module = "org.checkerframework:checker-qual", version.ref = "checkerFramework" }
 | 
			
		||||
cobalt = { module = "org.squiddev:Cobalt", version.ref = "cobalt" }
 | 
			
		||||
@@ -137,9 +139,17 @@ kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.
 | 
			
		||||
librarian = { module = "org.parchmentmc:librarian", version.ref = "librarian" }
 | 
			
		||||
minotaur = { module = "com.modrinth.minotaur:Minotaur", version.ref = "minotaur" }
 | 
			
		||||
nullAway = { module = "com.uber.nullaway:nullaway", version.ref = "nullAway" }
 | 
			
		||||
quiltflower = { module = "io.github.juuxel:loom-quiltflower", version.ref = "quiltflower" }
 | 
			
		||||
spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" }
 | 
			
		||||
teavm-classlib = { module = "org.teavm:teavm-classlib", version.ref = "teavm" }
 | 
			
		||||
teavm-jso = { module = "org.teavm:teavm-jso", version.ref = "teavm" }
 | 
			
		||||
teavm-jso-apis = { module = "org.teavm:teavm-jso-apis", version.ref = "teavm" }
 | 
			
		||||
teavm-jso-impl = { module = "org.teavm:teavm-jso-impl", version.ref = "teavm" }
 | 
			
		||||
teavm-metaprogramming-api = { module = "org.teavm:teavm-metaprogramming-api", version.ref = "teavm" }
 | 
			
		||||
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" }
 | 
			
		||||
 | 
			
		||||
[plugins]
 | 
			
		||||
forgeGradle = { id = "net.minecraftforge.gradle", version.ref = "forgeGradle" }
 | 
			
		||||
@@ -151,6 +161,7 @@ mixinGradle = { id = "org.spongepowered.mixin", version.ref = "mixinGradle" }
 | 
			
		||||
taskTree = { id = "com.dorongold.task-tree", version.ref = "taskTree" }
 | 
			
		||||
 | 
			
		||||
[bundles]
 | 
			
		||||
annotations = ["jsr305", "checkerFramework", "jetbrainsAnnotations"]
 | 
			
		||||
kotlin = ["kotlin-stdlib", "kotlin-coroutines"]
 | 
			
		||||
 | 
			
		||||
# Minecraft
 | 
			
		||||
@@ -164,3 +175,7 @@ externalMods-fabric-runtime = ["jei-fabric", "modmenu"]
 | 
			
		||||
# Testing
 | 
			
		||||
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" ]
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@
 | 
			
		||||
  /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
 | 
			
		||||
  /projects/web/src/mount)
 | 
			
		||||
  /projects/web/src/frontend/mount)
 | 
			
		||||
 | 
			
		||||
(doc
 | 
			
		||||
  ; Also defined in projects/web/build.gradle.kts
 | 
			
		||||
@@ -23,7 +23,7 @@
 | 
			
		||||
    (url https://tweaked.cc/)
 | 
			
		||||
    (source-link https://github.com/cc-tweaked/CC-Tweaked/blob/${commit}/${path}#L${line})
 | 
			
		||||
 | 
			
		||||
    (styles /projects/web/src/styles.css)
 | 
			
		||||
    (styles  /projects/web/build/rollup/index.css)
 | 
			
		||||
    (scripts /projects/web/build/rollup/index.js)
 | 
			
		||||
    (head doc/head.html))
 | 
			
		||||
 | 
			
		||||
@@ -115,4 +115,4 @@
 | 
			
		||||
      :max sleep write
 | 
			
		||||
      cct_test describe expect howlci fail it pending stub before_each)))
 | 
			
		||||
 | 
			
		||||
(at /projects/web/src/mount/expr_template.lua (lint (globals :max __expr__)))
 | 
			
		||||
(at /projects/web/src/frontend/mount/expr_template.lua (lint (globals :max __expr__)))
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3351
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3351
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										26
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								package.json
									
									
									
									
									
								
							@@ -6,24 +6,24 @@
 | 
			
		||||
  "license": "BSD-3-Clause",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@squid-dev/cc-web-term": "^2.0.0",
 | 
			
		||||
    "preact": "^10.5.5",
 | 
			
		||||
    "setimmediate": "^1.0.5",
 | 
			
		||||
    "tslib": "^2.0.3"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@rollup/plugin-terser": "^0.4.0",
 | 
			
		||||
    "@rollup/plugin-node-resolve": "^15.2.1",
 | 
			
		||||
    "@rollup/plugin-typescript": "^11.0.0",
 | 
			
		||||
    "@rollup/plugin-url": "^8.0.1",
 | 
			
		||||
    "@types/glob": "^8.1.0",
 | 
			
		||||
    "@types/react-dom": "^18.0.5",
 | 
			
		||||
    "glob": "^9.3.0",
 | 
			
		||||
    "react-dom": "^18.1.0",
 | 
			
		||||
    "react": "^18.1.0",
 | 
			
		||||
    "rehype-highlight": "^6.0.0",
 | 
			
		||||
    "rehype-react": "^7.1.1",
 | 
			
		||||
    "rehype": "^12.0.1",
 | 
			
		||||
    "requirejs": "^2.3.6",
 | 
			
		||||
    "rollup": "^3.19.1",
 | 
			
		||||
    "ts-node": "^10.8.0",
 | 
			
		||||
    "typescript": "^4.0.5"
 | 
			
		||||
    "@swc/core": "^1.3.92",
 | 
			
		||||
    "@types/node": "^20.8.3",
 | 
			
		||||
    "lightningcss": "^1.22.0",
 | 
			
		||||
    "preact-render-to-string": "^6.2.1",
 | 
			
		||||
    "rehype": "^13.0.0",
 | 
			
		||||
    "rehype-highlight": "^7.0.0",
 | 
			
		||||
    "rehype-react": "^8.0.0",
 | 
			
		||||
    "rollup": "^4.0.0",
 | 
			
		||||
    "tsx": "^3.12.10",
 | 
			
		||||
    "typescript": "^5.2.2"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ import org.joml.Matrix4f;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
class TurtleUpgradeModellers {
 | 
			
		||||
final class TurtleUpgradeModellers {
 | 
			
		||||
    private static final Transformation leftTransform = getMatrixFor(-0.4065f);
 | 
			
		||||
    private static final Transformation rightTransform = getMatrixFor(0.4065f);
 | 
			
		||||
 | 
			
		||||
@@ -35,7 +35,7 @@ class TurtleUpgradeModellers {
 | 
			
		||||
 | 
			
		||||
    static final TurtleUpgradeModeller<ITurtleUpgrade> UPGRADE_ITEM = new UpgradeItemModeller();
 | 
			
		||||
 | 
			
		||||
    private static class UpgradeItemModeller implements TurtleUpgradeModeller<ITurtleUpgrade> {
 | 
			
		||||
    private static final class UpgradeItemModeller implements TurtleUpgradeModeller<ITurtleUpgrade> {
 | 
			
		||||
        @Override
 | 
			
		||||
        public TransformedModel getModel(ITurtleUpgrade upgrade, @Nullable ITurtleAccess turtle, TurtleSide side) {
 | 
			
		||||
            return getModel(turtle == null ? upgrade.getCraftingItem() : upgrade.getUpgradeItem(turtle.getUpgradeNBTData(side)), side);
 | 
			
		||||
 
 | 
			
		||||
@@ -46,6 +46,14 @@ public class ComputerCraftTags {
 | 
			
		||||
        public static final TagKey<Block> WIRED_MODEM = make("wired_modem");
 | 
			
		||||
        public static final TagKey<Block> MONITOR = make("monitor");
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Blocks which should be ignored by a {@code peripheral_hub} peripheral.
 | 
			
		||||
         * <p>
 | 
			
		||||
         * This should include blocks which themselves expose a peripheral hub (such as {@linkplain #WIRED_MODEM wired
 | 
			
		||||
         * modems}).
 | 
			
		||||
         */
 | 
			
		||||
        public static final TagKey<Block> PERIPHERAL_HUB_IGNORE = make("peripheral_hub_ignore");
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Blocks which can be broken by any turtle tool.
 | 
			
		||||
         */
 | 
			
		||||
 
 | 
			
		||||
@@ -39,4 +39,6 @@ dependencies {
 | 
			
		||||
    testModImplementation(testFixtures(project(":core")))
 | 
			
		||||
    testModImplementation(testFixtures(project(":common")))
 | 
			
		||||
    testModImplementation(libs.bundles.kotlin)
 | 
			
		||||
 | 
			
		||||
    testFixturesImplementation(testFixtures(project(":core")))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: LicenseRef-CCPL
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.gui;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -47,7 +47,7 @@ public class ShaderMod {
 | 
			
		||||
        Optional<ShaderMod> get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static class Storage {
 | 
			
		||||
    private static final class Storage {
 | 
			
		||||
        static final ShaderMod INSTANCE = ServiceLoader.load(Provider.class)
 | 
			
		||||
            .stream()
 | 
			
		||||
            .flatMap(x -> x.get().get().stream())
 | 
			
		||||
 
 | 
			
		||||
@@ -139,8 +139,6 @@ public final class LanguageProvider implements DataProvider {
 | 
			
		||||
        add("commands.computercraft.tp.synopsis", "Teleport to a specific computer.");
 | 
			
		||||
        add("commands.computercraft.tp.desc", "Teleport to the location of a computer. You can either specify the computer's instance id (e.g. 123) or computer id (e.g #123).");
 | 
			
		||||
        add("commands.computercraft.tp.action", "Teleport to this computer");
 | 
			
		||||
        add("commands.computercraft.tp.not_player", "Cannot open terminal for non-player");
 | 
			
		||||
        add("commands.computercraft.tp.not_there", "Cannot locate computer in the world");
 | 
			
		||||
        add("commands.computercraft.view.synopsis", "View the terminal of a computer.");
 | 
			
		||||
        add("commands.computercraft.view.desc", "Open the terminal of a computer, allowing remote control of a computer. This does not provide access to turtle's inventories. You can either specify the computer's instance id (e.g. 123) or computer id (e.g #123).");
 | 
			
		||||
        add("commands.computercraft.view.action", "View this computer");
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
package dan200.computercraft.data;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import com.mojang.authlib.GameProfile;
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.api.pocket.PocketUpgradeDataProvider;
 | 
			
		||||
import dan200.computercraft.api.turtle.TurtleUpgradeDataProvider;
 | 
			
		||||
@@ -25,15 +26,12 @@ import net.minecraft.core.registries.Registries;
 | 
			
		||||
import net.minecraft.data.PackOutput;
 | 
			
		||||
import net.minecraft.data.recipes.*;
 | 
			
		||||
import net.minecraft.nbt.CompoundTag;
 | 
			
		||||
import net.minecraft.nbt.NbtUtils;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.tags.TagKey;
 | 
			
		||||
import net.minecraft.util.GsonHelper;
 | 
			
		||||
import net.minecraft.world.item.DyeColor;
 | 
			
		||||
import net.minecraft.world.item.DyeItem;
 | 
			
		||||
import net.minecraft.world.item.Item;
 | 
			
		||||
import net.minecraft.world.item.Items;
 | 
			
		||||
import net.minecraft.world.item.*;
 | 
			
		||||
import net.minecraft.world.item.crafting.Ingredient;
 | 
			
		||||
import net.minecraft.world.item.crafting.RecipeSerializer;
 | 
			
		||||
import net.minecraft.world.item.crafting.ShapedRecipe;
 | 
			
		||||
import net.minecraft.world.item.crafting.SimpleCraftingRecipeSerializer;
 | 
			
		||||
import net.minecraft.world.level.ItemLike;
 | 
			
		||||
@@ -41,6 +39,7 @@ import net.minecraft.world.level.block.Blocks;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Locale;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.api.ComputerCraftTags.Items.COMPUTER;
 | 
			
		||||
@@ -443,7 +442,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
 | 
			
		||||
            .requires(ModRegistry.Items.MONITOR_NORMAL.get())
 | 
			
		||||
            .unlockedBy("has_monitor", inventoryChange(ModRegistry.Items.MONITOR_NORMAL.get()))
 | 
			
		||||
            .save(
 | 
			
		||||
                RecipeWrapper.wrap(RecipeSerializer.SHAPELESS_RECIPE, add)
 | 
			
		||||
                RecipeWrapper.wrap(ModRegistry.RecipeSerializers.SHAPELESS.get(), add)
 | 
			
		||||
                    .withResultTag(playerHead("Cloudhunter", "6d074736-b1e9-4378-a99b-bd8777821c9c")),
 | 
			
		||||
                new ResourceLocation(ComputerCraftAPI.MOD_ID, "skull_cloudy")
 | 
			
		||||
            );
 | 
			
		||||
@@ -454,7 +453,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
 | 
			
		||||
            .requires(ModRegistry.Items.COMPUTER_ADVANCED.get())
 | 
			
		||||
            .unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_ADVANCED.get()))
 | 
			
		||||
            .save(
 | 
			
		||||
                RecipeWrapper.wrap(RecipeSerializer.SHAPELESS_RECIPE, add)
 | 
			
		||||
                RecipeWrapper.wrap(ModRegistry.RecipeSerializers.SHAPELESS.get(), add)
 | 
			
		||||
                    .withResultTag(playerHead("dan200", "f3c8d69b-0776-4512-8434-d1b2165909eb")),
 | 
			
		||||
                new ResourceLocation(ComputerCraftAPI.MOD_ID, "skull_dan200")
 | 
			
		||||
            );
 | 
			
		||||
@@ -513,17 +512,15 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static CompoundTag playerHead(String name, String uuid) {
 | 
			
		||||
        var owner = new CompoundTag();
 | 
			
		||||
        owner.putString("Name", name);
 | 
			
		||||
        owner.putString("Id", uuid);
 | 
			
		||||
        var owner = NbtUtils.writeGameProfile(new CompoundTag(), new GameProfile(UUID.fromString(uuid), name));
 | 
			
		||||
 | 
			
		||||
        var tag = new CompoundTag();
 | 
			
		||||
        tag.put("SkullOwner", owner);
 | 
			
		||||
        tag.put(PlayerHeadItem.TAG_SKULL_OWNER, owner);
 | 
			
		||||
        return tag;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static Consumer<JsonObject> family(ComputerFamily family) {
 | 
			
		||||
        return json -> json.addProperty("family", family.toString());
 | 
			
		||||
        return json -> json.addProperty("family", family.getSerializedName());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void addSpecial(Consumer<FinishedRecipe> add, SimpleCraftingRecipeSerializer<?> special) {
 | 
			
		||||
 
 | 
			
		||||
@@ -35,6 +35,8 @@ class TagProvider {
 | 
			
		||||
        tags.tag(ComputerCraftTags.Blocks.WIRED_MODEM).add(ModRegistry.Blocks.CABLE.get(), ModRegistry.Blocks.WIRED_MODEM_FULL.get());
 | 
			
		||||
        tags.tag(ComputerCraftTags.Blocks.MONITOR).add(ModRegistry.Blocks.MONITOR_NORMAL.get(), ModRegistry.Blocks.MONITOR_ADVANCED.get());
 | 
			
		||||
 | 
			
		||||
        tags.tag(ComputerCraftTags.Blocks.PERIPHERAL_HUB_IGNORE).addTag(ComputerCraftTags.Blocks.WIRED_MODEM);
 | 
			
		||||
 | 
			
		||||
        tags.tag(ComputerCraftTags.Blocks.TURTLE_ALWAYS_BREAKABLE).addTag(BlockTags.LEAVES).add(
 | 
			
		||||
            Blocks.BAMBOO, Blocks.BAMBOO_SAPLING // Bamboo isn't instabreak for some odd reason.
 | 
			
		||||
        );
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ final class WiredNetworkImpl implements WiredNetwork {
 | 
			
		||||
        nodes.add(node);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private WiredNetworkImpl(HashSet<WiredNodeImpl> nodes) {
 | 
			
		||||
    private WiredNetworkImpl(Set<WiredNodeImpl> nodes) {
 | 
			
		||||
        this.nodes = nodes;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -375,7 +375,7 @@ final class WiredNetworkImpl implements WiredNetwork {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static HashSet<WiredNodeImpl> reachableNodes(WiredNodeImpl start) {
 | 
			
		||||
    private static Set<WiredNodeImpl> reachableNodes(WiredNodeImpl start) {
 | 
			
		||||
        Queue<WiredNodeImpl> enqueued = new ArrayDeque<>();
 | 
			
		||||
        var reachable = new HashSet<WiredNodeImpl>();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -24,12 +24,14 @@ import dan200.computercraft.shared.common.ClearColourRecipe;
 | 
			
		||||
import dan200.computercraft.shared.common.ColourableRecipe;
 | 
			
		||||
import dan200.computercraft.shared.common.DefaultBundledRedstoneProvider;
 | 
			
		||||
import dan200.computercraft.shared.common.HeldItemMenu;
 | 
			
		||||
import dan200.computercraft.shared.computer.blocks.CommandComputerBlock;
 | 
			
		||||
import dan200.computercraft.shared.computer.blocks.CommandComputerBlockEntity;
 | 
			
		||||
import dan200.computercraft.shared.computer.blocks.ComputerBlock;
 | 
			
		||||
import dan200.computercraft.shared.computer.blocks.ComputerBlockEntity;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory;
 | 
			
		||||
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.data.BlockNamedEntityLootCondition;
 | 
			
		||||
@@ -68,6 +70,10 @@ import dan200.computercraft.shared.pocket.items.PocketComputerItem;
 | 
			
		||||
import dan200.computercraft.shared.pocket.peripherals.PocketModem;
 | 
			
		||||
import dan200.computercraft.shared.pocket.peripherals.PocketSpeaker;
 | 
			
		||||
import dan200.computercraft.shared.pocket.recipes.PocketComputerUpgradeRecipe;
 | 
			
		||||
import dan200.computercraft.shared.recipe.CustomShapedRecipe;
 | 
			
		||||
import dan200.computercraft.shared.recipe.CustomShapelessRecipe;
 | 
			
		||||
import dan200.computercraft.shared.recipe.ImpostorShapedRecipe;
 | 
			
		||||
import dan200.computercraft.shared.recipe.ImpostorShapelessRecipe;
 | 
			
		||||
import dan200.computercraft.shared.turtle.FurnaceRefuelHandler;
 | 
			
		||||
import dan200.computercraft.shared.turtle.blocks.TurtleBlock;
 | 
			
		||||
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
 | 
			
		||||
@@ -77,8 +83,6 @@ import dan200.computercraft.shared.turtle.recipes.TurtleOverlayRecipe;
 | 
			
		||||
import dan200.computercraft.shared.turtle.recipes.TurtleRecipe;
 | 
			
		||||
import dan200.computercraft.shared.turtle.recipes.TurtleUpgradeRecipe;
 | 
			
		||||
import dan200.computercraft.shared.turtle.upgrades.*;
 | 
			
		||||
import dan200.computercraft.shared.util.ImpostorRecipe;
 | 
			
		||||
import dan200.computercraft.shared.util.ImpostorShapelessRecipe;
 | 
			
		||||
import net.minecraft.commands.CommandSourceStack;
 | 
			
		||||
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
 | 
			
		||||
import net.minecraft.commands.synchronization.SingletonArgumentInfo;
 | 
			
		||||
@@ -139,7 +143,7 @@ public final class ModRegistry {
 | 
			
		||||
        public static final RegistryEntry<ComputerBlock<ComputerBlockEntity>> COMPUTER_ADVANCED = REGISTRY.register("computer_advanced",
 | 
			
		||||
            () -> new ComputerBlock<>(computerProperties().mapColor(MapColor.GOLD), ComputerFamily.ADVANCED, BlockEntities.COMPUTER_ADVANCED));
 | 
			
		||||
 | 
			
		||||
        public static final RegistryEntry<ComputerBlock<CommandComputerBlockEntity>> COMPUTER_COMMAND = REGISTRY.register("computer_command", () -> new ComputerBlock<>(
 | 
			
		||||
        public static final RegistryEntry<ComputerBlock<CommandComputerBlockEntity>> COMPUTER_COMMAND = REGISTRY.register("computer_command", () -> new CommandComputerBlock<>(
 | 
			
		||||
            computerProperties().strength(-1, 6000000.0F),
 | 
			
		||||
            ComputerFamily.COMMAND, BlockEntities.COMPUTER_COMMAND
 | 
			
		||||
        ));
 | 
			
		||||
@@ -222,7 +226,7 @@ public final class ModRegistry {
 | 
			
		||||
 | 
			
		||||
        public static final RegistryEntry<ComputerItem> COMPUTER_NORMAL = ofBlock(Blocks.COMPUTER_NORMAL, ComputerItem::new);
 | 
			
		||||
        public static final RegistryEntry<ComputerItem> COMPUTER_ADVANCED = ofBlock(Blocks.COMPUTER_ADVANCED, ComputerItem::new);
 | 
			
		||||
        public static final RegistryEntry<ComputerItem> COMPUTER_COMMAND = ofBlock(Blocks.COMPUTER_COMMAND, ComputerItem::new);
 | 
			
		||||
        public static final RegistryEntry<ComputerItem> COMPUTER_COMMAND = ofBlock(Blocks.COMPUTER_COMMAND, CommandComputerItem::new);
 | 
			
		||||
 | 
			
		||||
        public static final RegistryEntry<PocketComputerItem> POCKET_COMPUTER_NORMAL = REGISTRY.register("pocket_computer_normal",
 | 
			
		||||
            () -> new PocketComputerItem(properties().stacksTo(1), ComputerFamily.NORMAL));
 | 
			
		||||
@@ -357,17 +361,21 @@ public final class ModRegistry {
 | 
			
		||||
            return REGISTRY.register(name, () -> new SimpleCraftingRecipeSerializer<>(factory));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static final RegistryEntry<RecipeSerializer<CustomShapedRecipe>> SHAPED = REGISTRY.register("shaped", () -> CustomShapedRecipe.serialiser(CustomShapedRecipe::new));
 | 
			
		||||
        public static final RegistryEntry<RecipeSerializer<CustomShapelessRecipe>> SHAPELESS = REGISTRY.register("shapeless", () -> CustomShapelessRecipe.serialiser(CustomShapelessRecipe::new));
 | 
			
		||||
 | 
			
		||||
        public static final RegistryEntry<RecipeSerializer<ImpostorShapedRecipe>> IMPOSTOR_SHAPED = REGISTRY.register("impostor_shaped", () -> CustomShapedRecipe.serialiser(ImpostorShapedRecipe::new));
 | 
			
		||||
        public static final RegistryEntry<RecipeSerializer<ImpostorShapelessRecipe>> IMPOSTOR_SHAPELESS = REGISTRY.register("impostor_shapeless", () -> CustomShapelessRecipe.serialiser(ImpostorShapelessRecipe::new));
 | 
			
		||||
 | 
			
		||||
        public static final RegistryEntry<SimpleCraftingRecipeSerializer<ColourableRecipe>> DYEABLE_ITEM = simple("colour", ColourableRecipe::new);
 | 
			
		||||
        public static final RegistryEntry<SimpleCraftingRecipeSerializer<ClearColourRecipe>> DYEABLE_ITEM_CLEAR = simple("clear_colour", ClearColourRecipe::new);
 | 
			
		||||
        public static final RegistryEntry<TurtleRecipe.Serializer> TURTLE = REGISTRY.register("turtle", TurtleRecipe.Serializer::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<TurtleOverlayRecipe.Serializer> TURTLE_OVERLAY = REGISTRY.register("turtle_overlay", TurtleOverlayRecipe.Serializer::new);
 | 
			
		||||
        public static final RegistryEntry<RecipeSerializer<TurtleOverlayRecipe>> TURTLE_OVERLAY = REGISTRY.register("turtle_overlay", TurtleOverlayRecipe.Serializer::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<ComputerUpgradeRecipe.Serializer> COMPUTER_UPGRADE = REGISTRY.register("computer_upgrade", ComputerUpgradeRecipe.Serializer::new);
 | 
			
		||||
        public static final RegistryEntry<ImpostorRecipe.Serializer> IMPOSTOR_SHAPED = REGISTRY.register("impostor_shaped", ImpostorRecipe.Serializer::new);
 | 
			
		||||
        public static final RegistryEntry<ImpostorShapelessRecipe.Serializer> IMPOSTOR_SHAPELESS = REGISTRY.register("impostor_shapeless", ImpostorShapelessRecipe.Serializer::new);
 | 
			
		||||
        public static final RegistryEntry<RecipeSerializer<ComputerUpgradeRecipe>> COMPUTER_UPGRADE = REGISTRY.register("computer_upgrade", ComputerUpgradeRecipe.Serializer::new);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static class Permissions {
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,6 @@ import dan200.computercraft.shared.network.container.ComputerContainerData;
 | 
			
		||||
import net.minecraft.commands.CommandSourceStack;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.server.level.ServerPlayer;
 | 
			
		||||
import net.minecraft.world.MenuProvider;
 | 
			
		||||
import net.minecraft.world.entity.RelativeMovement;
 | 
			
		||||
import net.minecraft.world.entity.player.Inventory;
 | 
			
		||||
@@ -34,13 +33,15 @@ import net.minecraft.world.entity.player.Player;
 | 
			
		||||
import net.minecraft.world.inventory.AbstractContainerMenu;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
import net.minecraft.world.phys.Vec3;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.shared.command.CommandUtils.isPlayer;
 | 
			
		||||
import static dan200.computercraft.shared.command.Exceptions.*;
 | 
			
		||||
import static dan200.computercraft.shared.command.Exceptions.NOT_TRACKING_EXCEPTION;
 | 
			
		||||
import static dan200.computercraft.shared.command.Exceptions.NO_TIMINGS_EXCEPTION;
 | 
			
		||||
import static dan200.computercraft.shared.command.arguments.ComputerArgumentType.getComputerArgument;
 | 
			
		||||
import static dan200.computercraft.shared.command.arguments.ComputerArgumentType.oneComputer;
 | 
			
		||||
import static dan200.computercraft.shared.command.arguments.ComputersArgumentType.*;
 | 
			
		||||
@@ -62,118 +63,25 @@ public final class CommandComputerCraft {
 | 
			
		||||
        dispatcher.register(choice("computercraft")
 | 
			
		||||
            .then(literal("dump")
 | 
			
		||||
                .requires(ModRegistry.Permissions.PERMISSION_DUMP)
 | 
			
		||||
                .executes(context -> {
 | 
			
		||||
                    var table = new TableBuilder("DumpAll", "Computer", "On", "Position");
 | 
			
		||||
 | 
			
		||||
                    var source = context.getSource();
 | 
			
		||||
                    List<ServerComputer> computers = new ArrayList<>(ServerContext.get(source.getServer()).registry().getComputers());
 | 
			
		||||
 | 
			
		||||
                    Level world = source.getLevel();
 | 
			
		||||
                    var pos = BlockPos.containing(source.getPosition());
 | 
			
		||||
 | 
			
		||||
                    computers.sort((a, b) -> {
 | 
			
		||||
                        if (a.getLevel() == b.getLevel() && a.getLevel() == world) {
 | 
			
		||||
                            return Double.compare(a.getPosition().distSqr(pos), b.getPosition().distSqr(pos));
 | 
			
		||||
                        } else if (a.getLevel() == world) {
 | 
			
		||||
                            return -1;
 | 
			
		||||
                        } else if (b.getLevel() == world) {
 | 
			
		||||
                            return 1;
 | 
			
		||||
                        } else {
 | 
			
		||||
                            return Integer.compare(a.getInstanceID(), b.getInstanceID());
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    for (var computer : computers) {
 | 
			
		||||
                        table.row(
 | 
			
		||||
                            linkComputer(source, computer, computer.getID()),
 | 
			
		||||
                            bool(computer.isOn()),
 | 
			
		||||
                            linkPosition(source, computer)
 | 
			
		||||
                        );
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    table.display(context.getSource());
 | 
			
		||||
                    return computers.size();
 | 
			
		||||
                })
 | 
			
		||||
                .executes(c -> dump(c.getSource()))
 | 
			
		||||
                .then(args()
 | 
			
		||||
                    .arg("computer", oneComputer())
 | 
			
		||||
                    .executes(context -> {
 | 
			
		||||
                        var computer = getComputerArgument(context, "computer");
 | 
			
		||||
 | 
			
		||||
                        var table = new TableBuilder("Dump");
 | 
			
		||||
                        table.row(header("Instance"), text(Integer.toString(computer.getInstanceID())));
 | 
			
		||||
                        table.row(header("Id"), text(Integer.toString(computer.getID())));
 | 
			
		||||
                        table.row(header("Label"), text(computer.getLabel()));
 | 
			
		||||
                        table.row(header("On"), bool(computer.isOn()));
 | 
			
		||||
                        table.row(header("Position"), linkPosition(context.getSource(), computer));
 | 
			
		||||
                        table.row(header("Family"), text(computer.getFamily().toString()));
 | 
			
		||||
 | 
			
		||||
                        for (var side : ComputerSide.values()) {
 | 
			
		||||
                            var peripheral = computer.getPeripheral(side);
 | 
			
		||||
                            if (peripheral != null) {
 | 
			
		||||
                                table.row(header("Peripheral " + side.getName()), text(peripheral.getType()));
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        table.display(context.getSource());
 | 
			
		||||
                        return 1;
 | 
			
		||||
                    })))
 | 
			
		||||
                    .executes(c -> dumpComputer(c.getSource(), getComputerArgument(c, "computer")))))
 | 
			
		||||
 | 
			
		||||
            .then(command("shutdown")
 | 
			
		||||
                .requires(ModRegistry.Permissions.PERMISSION_SHUTDOWN)
 | 
			
		||||
                .argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers())
 | 
			
		||||
                .executes((context, computerSelectors) -> {
 | 
			
		||||
                    var shutdown = 0;
 | 
			
		||||
                    var computers = unwrap(context.getSource(), computerSelectors);
 | 
			
		||||
                    for (var computer : computers) {
 | 
			
		||||
                        if (computer.isOn()) shutdown++;
 | 
			
		||||
                        computer.shutdown();
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    var didShutdown = shutdown;
 | 
			
		||||
                    context.getSource().sendSuccess(() -> Component.translatable("commands.computercraft.shutdown.done", didShutdown, computers.size()), false);
 | 
			
		||||
                    return shutdown;
 | 
			
		||||
                }))
 | 
			
		||||
                .executes((c, a) -> shutdown(c.getSource(), unwrap(c.getSource(), a))))
 | 
			
		||||
 | 
			
		||||
            .then(command("turn-on")
 | 
			
		||||
                .requires(ModRegistry.Permissions.PERMISSION_TURN_ON)
 | 
			
		||||
                .argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers())
 | 
			
		||||
                .executes((context, computerSelectors) -> {
 | 
			
		||||
                    var on = 0;
 | 
			
		||||
                    var computers = unwrap(context.getSource(), computerSelectors);
 | 
			
		||||
                    for (var computer : computers) {
 | 
			
		||||
                        if (!computer.isOn()) on++;
 | 
			
		||||
                        computer.turnOn();
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    var didOn = on;
 | 
			
		||||
                    context.getSource().sendSuccess(() -> Component.translatable("commands.computercraft.turn_on.done", didOn, computers.size()), false);
 | 
			
		||||
                    return on;
 | 
			
		||||
                }))
 | 
			
		||||
                .executes((c, a) -> turnOn(c.getSource(), unwrap(c.getSource(), a))))
 | 
			
		||||
 | 
			
		||||
            .then(command("tp")
 | 
			
		||||
                .requires(ModRegistry.Permissions.PERMISSION_TP)
 | 
			
		||||
                .arg("computer", oneComputer())
 | 
			
		||||
                .executes(context -> {
 | 
			
		||||
                    var computer = getComputerArgument(context, "computer");
 | 
			
		||||
                    var world = computer.getLevel();
 | 
			
		||||
                    var pos = computer.getPosition();
 | 
			
		||||
 | 
			
		||||
                    var entity = context.getSource().getEntityOrException();
 | 
			
		||||
                    if (!(entity instanceof ServerPlayer player)) throw TP_NOT_PLAYER.create();
 | 
			
		||||
 | 
			
		||||
                    if (player.getCommandSenderWorld() == world) {
 | 
			
		||||
                        player.connection.teleport(
 | 
			
		||||
                            pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5, 0, 0,
 | 
			
		||||
                            EnumSet.noneOf(RelativeMovement.class)
 | 
			
		||||
                        );
 | 
			
		||||
                    } else {
 | 
			
		||||
                        player.teleportTo(world,
 | 
			
		||||
                            pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5, 0, 0
 | 
			
		||||
                        );
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return 1;
 | 
			
		||||
                }))
 | 
			
		||||
                .executes(c -> teleport(c.getSource(), getComputerArgument(c, "computer"))))
 | 
			
		||||
 | 
			
		||||
            .then(command("queue")
 | 
			
		||||
                .requires(ModRegistry.Permissions.PERMISSION_QUEUE)
 | 
			
		||||
@@ -182,79 +90,243 @@ public final class CommandComputerCraft {
 | 
			
		||||
                        .suggests((context, builder) -> Suggestions.empty())
 | 
			
		||||
                )
 | 
			
		||||
                .argManyValue("args", StringArgumentType.string(), Collections.emptyList())
 | 
			
		||||
                .executes((ctx, args) -> {
 | 
			
		||||
                    var computers = getComputersArgument(ctx, "computer");
 | 
			
		||||
                    var rest = args.toArray();
 | 
			
		||||
 | 
			
		||||
                    var queued = 0;
 | 
			
		||||
                    for (var computer : computers) {
 | 
			
		||||
                        if (computer.getFamily() == ComputerFamily.COMMAND && computer.isOn()) {
 | 
			
		||||
                            computer.queueEvent("computer_command", rest);
 | 
			
		||||
                            queued++;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return queued;
 | 
			
		||||
                }))
 | 
			
		||||
                .executes((c, a) -> queue(getComputersArgument(c, "computer"), a)))
 | 
			
		||||
 | 
			
		||||
            .then(command("view")
 | 
			
		||||
                .requires(ModRegistry.Permissions.PERMISSION_VIEW)
 | 
			
		||||
                .arg("computer", oneComputer())
 | 
			
		||||
                .executes(context -> {
 | 
			
		||||
                    var player = context.getSource().getPlayerOrException();
 | 
			
		||||
                    var computer = getComputerArgument(context, "computer");
 | 
			
		||||
                    new ComputerContainerData(computer, ItemStack.EMPTY).open(player, new MenuProvider() {
 | 
			
		||||
                        @Override
 | 
			
		||||
                        public Component getDisplayName() {
 | 
			
		||||
                            return Component.translatable("gui.computercraft.view_computer");
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        @Override
 | 
			
		||||
                        public AbstractContainerMenu createMenu(int id, Inventory player, Player entity) {
 | 
			
		||||
                            return new ViewComputerMenu(id, player, computer);
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
                    return 1;
 | 
			
		||||
                }))
 | 
			
		||||
                .executes(c -> view(c.getSource(), getComputerArgument(c, "computer"))))
 | 
			
		||||
 | 
			
		||||
            .then(choice("track")
 | 
			
		||||
                .requires(ModRegistry.Permissions.PERMISSION_TRACK)
 | 
			
		||||
                .then(command("start")
 | 
			
		||||
                    .executes(context -> {
 | 
			
		||||
                        getMetricsInstance(context.getSource()).start();
 | 
			
		||||
 | 
			
		||||
                        var stopCommand = "/computercraft track stop";
 | 
			
		||||
                        context.getSource().sendSuccess(() -> Component.translatable(
 | 
			
		||||
                            "commands.computercraft.track.start.stop",
 | 
			
		||||
                            link(text(stopCommand), stopCommand, Component.translatable("commands.computercraft.track.stop.action"))
 | 
			
		||||
                        ), false);
 | 
			
		||||
                        return 1;
 | 
			
		||||
                    }))
 | 
			
		||||
 | 
			
		||||
                .then(command("stop")
 | 
			
		||||
                    .executes(context -> {
 | 
			
		||||
                        var timings = getMetricsInstance(context.getSource());
 | 
			
		||||
                        if (!timings.stop()) throw NOT_TRACKING_EXCEPTION.create();
 | 
			
		||||
                        displayTimings(context.getSource(), timings.getSnapshot(), new AggregatedMetric(Metrics.COMPUTER_TASKS, Aggregate.AVG), DEFAULT_FIELDS);
 | 
			
		||||
                        return 1;
 | 
			
		||||
                    }))
 | 
			
		||||
 | 
			
		||||
                .then(command("start").executes(c -> trackStart(c.getSource())))
 | 
			
		||||
                .then(command("stop").executes(c -> trackStop(c.getSource())))
 | 
			
		||||
                .then(command("dump")
 | 
			
		||||
                    .argManyValue("fields", metric(), DEFAULT_FIELDS)
 | 
			
		||||
                    .executes((context, fields) -> {
 | 
			
		||||
                        AggregatedMetric sort;
 | 
			
		||||
                        if (fields.size() == 1 && DEFAULT_FIELDS.contains(fields.get(0))) {
 | 
			
		||||
                            sort = fields.get(0);
 | 
			
		||||
                            fields = DEFAULT_FIELDS;
 | 
			
		||||
                        } else {
 | 
			
		||||
                            sort = fields.get(0);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        return displayTimings(context.getSource(), sort, fields);
 | 
			
		||||
                    })))
 | 
			
		||||
                    .executes((c, f) -> trackDump(c.getSource(), f))))
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Display loaded computers to a table.
 | 
			
		||||
     *
 | 
			
		||||
     * @param source The thing that executed this command.
 | 
			
		||||
     * @return The number of loaded computers.
 | 
			
		||||
     */
 | 
			
		||||
    private static int dump(CommandSourceStack source) {
 | 
			
		||||
        var table = new TableBuilder("DumpAll", "Computer", "On", "Position");
 | 
			
		||||
 | 
			
		||||
        List<ServerComputer> computers = new ArrayList<>(ServerContext.get(source.getServer()).registry().getComputers());
 | 
			
		||||
 | 
			
		||||
        Level world = source.getLevel();
 | 
			
		||||
        var pos = BlockPos.containing(source.getPosition());
 | 
			
		||||
 | 
			
		||||
        // Sort by nearby computers.
 | 
			
		||||
        computers.sort((a, b) -> {
 | 
			
		||||
            if (a.getLevel() == b.getLevel() && a.getLevel() == world) {
 | 
			
		||||
                return Double.compare(a.getPosition().distSqr(pos), b.getPosition().distSqr(pos));
 | 
			
		||||
            } else if (a.getLevel() == world) {
 | 
			
		||||
                return -1;
 | 
			
		||||
            } else if (b.getLevel() == world) {
 | 
			
		||||
                return 1;
 | 
			
		||||
            } else {
 | 
			
		||||
                return Integer.compare(a.getInstanceID(), b.getInstanceID());
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        for (var computer : computers) {
 | 
			
		||||
            table.row(
 | 
			
		||||
                linkComputer(source, computer, computer.getID()),
 | 
			
		||||
                bool(computer.isOn()),
 | 
			
		||||
                linkPosition(source, computer)
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        table.display(source);
 | 
			
		||||
        return computers.size();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Display additional information about a single computer.
 | 
			
		||||
     *
 | 
			
		||||
     * @param source   The thing that executed this command.
 | 
			
		||||
     * @param computer The computer we're dumping.
 | 
			
		||||
     * @return The constant {@code 1}.
 | 
			
		||||
     */
 | 
			
		||||
    private static int dumpComputer(CommandSourceStack source, ServerComputer computer) {
 | 
			
		||||
        var table = new TableBuilder("Dump");
 | 
			
		||||
        table.row(header("Instance"), text(Integer.toString(computer.getInstanceID())));
 | 
			
		||||
        table.row(header("Id"), text(Integer.toString(computer.getID())));
 | 
			
		||||
        table.row(header("Label"), text(computer.getLabel()));
 | 
			
		||||
        table.row(header("On"), bool(computer.isOn()));
 | 
			
		||||
        table.row(header("Position"), linkPosition(source, computer));
 | 
			
		||||
        table.row(header("Family"), text(computer.getFamily().toString()));
 | 
			
		||||
 | 
			
		||||
        for (var side : ComputerSide.values()) {
 | 
			
		||||
            var peripheral = computer.getPeripheral(side);
 | 
			
		||||
            if (peripheral != null) {
 | 
			
		||||
                table.row(header("Peripheral " + side.getName()), text(peripheral.getType()));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        table.display(source);
 | 
			
		||||
        return 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Shutdown a list of computers.
 | 
			
		||||
     *
 | 
			
		||||
     * @param source    The thing that executed this command.
 | 
			
		||||
     * @param computers The computers to shutdown.
 | 
			
		||||
     * @return The constant {@code 1}.
 | 
			
		||||
     */
 | 
			
		||||
    private static int shutdown(CommandSourceStack source, Collection<ServerComputer> computers) {
 | 
			
		||||
        var shutdown = 0;
 | 
			
		||||
        for (var computer : computers) {
 | 
			
		||||
            if (computer.isOn()) shutdown++;
 | 
			
		||||
            computer.shutdown();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var didShutdown = shutdown;
 | 
			
		||||
        source.sendSuccess(() -> Component.translatable("commands.computercraft.shutdown.done", didShutdown, computers.size()), false);
 | 
			
		||||
        return shutdown;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Turn on a list of computers.
 | 
			
		||||
     *
 | 
			
		||||
     * @param source    The thing that executed this command.
 | 
			
		||||
     * @param computers The computers to turn on.
 | 
			
		||||
     * @return The constant {@code 1}.
 | 
			
		||||
     */
 | 
			
		||||
    private static int turnOn(CommandSourceStack source, Collection<ServerComputer> computers) {
 | 
			
		||||
        var on = 0;
 | 
			
		||||
        for (var computer : computers) {
 | 
			
		||||
            if (!computer.isOn()) on++;
 | 
			
		||||
            computer.turnOn();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var didOn = on;
 | 
			
		||||
        source.sendSuccess(() -> Component.translatable("commands.computercraft.turn_on.done", didOn, computers.size()), false);
 | 
			
		||||
        return on;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Teleport to a computer.
 | 
			
		||||
     *
 | 
			
		||||
     * @param source   The thing that executed this command. This must be an entity, other types will throw an exception.
 | 
			
		||||
     * @param computer The computer to teleport to.
 | 
			
		||||
     * @return The constant {@code 1}.
 | 
			
		||||
     */
 | 
			
		||||
    private static int teleport(CommandSourceStack source, ServerComputer computer) throws CommandSyntaxException {
 | 
			
		||||
        var world = computer.getLevel();
 | 
			
		||||
        var pos = Vec3.atBottomCenterOf(computer.getPosition());
 | 
			
		||||
        source.getEntityOrException().teleportTo(world, pos.x(), pos.y(), pos.z(), EnumSet.noneOf(RelativeMovement.class), 0, 0);
 | 
			
		||||
 | 
			
		||||
        return 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Queue a {@code computer_command} event on a command computer.
 | 
			
		||||
     *
 | 
			
		||||
     * @param computers The list of computers to queue on.
 | 
			
		||||
     * @param args      The arguments for this event.
 | 
			
		||||
     * @return The number of computers this event was queued on.
 | 
			
		||||
     */
 | 
			
		||||
    private static int queue(Collection<ServerComputer> computers, List<String> args) {
 | 
			
		||||
        var rest = args.toArray();
 | 
			
		||||
 | 
			
		||||
        var queued = 0;
 | 
			
		||||
        for (var computer : computers) {
 | 
			
		||||
            if (computer.getFamily() == ComputerFamily.COMMAND && computer.isOn()) {
 | 
			
		||||
                computer.queueEvent("computer_command", rest);
 | 
			
		||||
                queued++;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return queued;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Open a terminal for a computer.
 | 
			
		||||
     *
 | 
			
		||||
     * @param source   The thing that executed this command.
 | 
			
		||||
     * @param computer The computer to view.
 | 
			
		||||
     * @return The constant {@code 1}.
 | 
			
		||||
     */
 | 
			
		||||
    private static int view(CommandSourceStack source, ServerComputer computer) throws CommandSyntaxException {
 | 
			
		||||
        var player = source.getPlayerOrException();
 | 
			
		||||
        new ComputerContainerData(computer, ItemStack.EMPTY).open(player, new MenuProvider() {
 | 
			
		||||
            @Override
 | 
			
		||||
            public Component getDisplayName() {
 | 
			
		||||
                return Component.translatable("gui.computercraft.view_computer");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public AbstractContainerMenu createMenu(int id, Inventory player, Player entity) {
 | 
			
		||||
                return new ViewComputerMenu(id, player, computer);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        return 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Start tracking metrics for the current player.
 | 
			
		||||
     *
 | 
			
		||||
     * @param source The thing that executed this command.
 | 
			
		||||
     * @return The constant {@code 1}.
 | 
			
		||||
     */
 | 
			
		||||
    private static int trackStart(CommandSourceStack source) {
 | 
			
		||||
        getMetricsInstance(source).start();
 | 
			
		||||
 | 
			
		||||
        var stopCommand = "/computercraft track stop";
 | 
			
		||||
        source.sendSuccess(() -> Component.translatable(
 | 
			
		||||
            "commands.computercraft.track.start.stop",
 | 
			
		||||
            link(text(stopCommand), stopCommand, Component.translatable("commands.computercraft.track.stop.action"))
 | 
			
		||||
        ), false);
 | 
			
		||||
        return 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Stop tracking metrics for the current player, displaying a table with the results.
 | 
			
		||||
     *
 | 
			
		||||
     * @param source The thing that executed this command.
 | 
			
		||||
     * @return The constant {@code 1}.
 | 
			
		||||
     */
 | 
			
		||||
    private static int trackStop(CommandSourceStack source) throws CommandSyntaxException {
 | 
			
		||||
        var metrics = getMetricsInstance(source);
 | 
			
		||||
        if (!metrics.stop()) throw NOT_TRACKING_EXCEPTION.create();
 | 
			
		||||
        displayTimings(source, metrics.getSnapshot(), new AggregatedMetric(Metrics.COMPUTER_TASKS, Aggregate.AVG), DEFAULT_FIELDS);
 | 
			
		||||
        return 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static final List<AggregatedMetric> DEFAULT_FIELDS = Arrays.asList(
 | 
			
		||||
        new AggregatedMetric(Metrics.COMPUTER_TASKS, Aggregate.COUNT),
 | 
			
		||||
        new AggregatedMetric(Metrics.COMPUTER_TASKS, Aggregate.NONE),
 | 
			
		||||
        new AggregatedMetric(Metrics.COMPUTER_TASKS, Aggregate.AVG)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Display the latest metrics for the current player.
 | 
			
		||||
     *
 | 
			
		||||
     * @param source The thing that executed this command.
 | 
			
		||||
     * @param fields The fields to display in this table, defaulting to {@link #DEFAULT_FIELDS}.
 | 
			
		||||
     * @return The constant {@code 1}.
 | 
			
		||||
     */
 | 
			
		||||
    private static int trackDump(CommandSourceStack source, List<AggregatedMetric> fields) throws CommandSyntaxException {
 | 
			
		||||
        AggregatedMetric sort;
 | 
			
		||||
        if (fields.size() == 1 && DEFAULT_FIELDS.contains(fields.get(0))) {
 | 
			
		||||
            sort = fields.get(0);
 | 
			
		||||
            fields = DEFAULT_FIELDS;
 | 
			
		||||
        } else {
 | 
			
		||||
            sort = fields.get(0);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return displayTimings(source, getMetricsInstance(source).getTimings(), sort, fields);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Additional helper functions.
 | 
			
		||||
 | 
			
		||||
    private static Component linkComputer(CommandSourceStack source, @Nullable ServerComputer serverComputer, int computerId) {
 | 
			
		||||
        var out = Component.literal("");
 | 
			
		||||
 | 
			
		||||
@@ -327,16 +399,6 @@ public final class CommandComputerCraft {
 | 
			
		||||
        return ServerContext.get(source.getServer()).metrics().getMetricsInstance(entity instanceof Player ? entity.getUUID() : SYSTEM_UUID);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static final List<AggregatedMetric> DEFAULT_FIELDS = Arrays.asList(
 | 
			
		||||
        new AggregatedMetric(Metrics.COMPUTER_TASKS, Aggregate.COUNT),
 | 
			
		||||
        new AggregatedMetric(Metrics.COMPUTER_TASKS, Aggregate.NONE),
 | 
			
		||||
        new AggregatedMetric(Metrics.COMPUTER_TASKS, Aggregate.AVG)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    private static int displayTimings(CommandSourceStack source, AggregatedMetric sortField, List<AggregatedMetric> fields) throws CommandSyntaxException {
 | 
			
		||||
        return displayTimings(source, getMetricsInstance(source).getTimings(), sortField, fields);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static int displayTimings(CommandSourceStack source, List<ComputerMetrics> timings, AggregatedMetric sortField, List<AggregatedMetric> fields) throws CommandSyntaxException {
 | 
			
		||||
        if (timings.isEmpty()) throw NO_TIMINGS_EXCEPTION.create();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,9 +18,6 @@ public final class Exceptions {
 | 
			
		||||
    static final SimpleCommandExceptionType NOT_TRACKING_EXCEPTION = translated("commands.computercraft.track.stop.not_enabled");
 | 
			
		||||
    static final SimpleCommandExceptionType NO_TIMINGS_EXCEPTION = translated("commands.computercraft.track.dump.no_timings");
 | 
			
		||||
 | 
			
		||||
    static final SimpleCommandExceptionType TP_NOT_THERE = translated("commands.computercraft.tp.not_there");
 | 
			
		||||
    static final SimpleCommandExceptionType TP_NOT_PLAYER = translated("commands.computercraft.tp.not_player");
 | 
			
		||||
 | 
			
		||||
    public static final SimpleCommandExceptionType ARGUMENT_EXPECTED = translated("argument.computercraft.argument_expected");
 | 
			
		||||
 | 
			
		||||
    private static SimpleCommandExceptionType translated(String key) {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A subclass of {@link ComputerBlock} which implements {@link GameMasterBlock}, to prevent players breaking it without
 | 
			
		||||
 * permission.
 | 
			
		||||
 *
 | 
			
		||||
 * @param <T> The type of the computer block entity.
 | 
			
		||||
 * @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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -104,11 +104,15 @@ public class CommandComputerBlockEntity extends ComputerBlockEntity {
 | 
			
		||||
        if (server == null || !server.isCommandBlockEnabled()) {
 | 
			
		||||
            player.displayClientMessage(Component.translatable("advMode.notEnabled"), true);
 | 
			
		||||
            return false;
 | 
			
		||||
        } else if (Config.commandRequireCreative ? !player.canUseGameMasterBlocks() : !server.getPlayerList().isOp(player.getGameProfile())) {
 | 
			
		||||
        } else if (!canUseCommandBlock(player)) {
 | 
			
		||||
            player.displayClientMessage(Component.translatable("advMode.notAllowed"), true);
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static boolean canUseCommandBlock(Player player) {
 | 
			
		||||
        return Config.commandRequireCreative ? player.canUseGameMasterBlocks() : player.hasPermissions(2);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,33 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.computer.core;
 | 
			
		||||
 | 
			
		||||
public enum ComputerFamily {
 | 
			
		||||
    NORMAL,
 | 
			
		||||
    ADVANCED,
 | 
			
		||||
    COMMAND
 | 
			
		||||
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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -29,8 +29,6 @@ import java.util.Map;
 | 
			
		||||
public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
 | 
			
		||||
    private static final Logger LOG = LoggerFactory.getLogger(ResourceMount.class);
 | 
			
		||||
 | 
			
		||||
    private static final byte[] TEMP_BUFFER = new byte[8192];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Maintain a cache of currently loaded resource mounts. This cache is invalidated when currentManager changes.
 | 
			
		||||
     */
 | 
			
		||||
@@ -60,7 +58,7 @@ public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
 | 
			
		||||
        var hasAny = false;
 | 
			
		||||
        String existingNamespace = null;
 | 
			
		||||
 | 
			
		||||
        var newRoot = new FileEntry("", new ResourceLocation(namespace, subPath));
 | 
			
		||||
        var newRoot = new FileEntry(new ResourceLocation(namespace, subPath));
 | 
			
		||||
        for (var file : manager.listResources(subPath, s -> true).keySet()) {
 | 
			
		||||
            existingNamespace = file.getNamespace();
 | 
			
		||||
 | 
			
		||||
@@ -68,7 +66,11 @@ public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
 | 
			
		||||
            if (!FileSystem.contains(subPath, file.getPath())) continue; // Some packs seem to include the parent?
 | 
			
		||||
 | 
			
		||||
            var localPath = FileSystem.toLocal(file.getPath(), subPath);
 | 
			
		||||
            create(newRoot, localPath);
 | 
			
		||||
            try {
 | 
			
		||||
                getOrCreateChild(newRoot, localPath, this::createEntry);
 | 
			
		||||
            } catch (ResourceLocationException e) {
 | 
			
		||||
                LOG.warn("Cannot create resource location for {} ({})", localPath, e.getMessage());
 | 
			
		||||
            }
 | 
			
		||||
            hasAny = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -83,65 +85,24 @@ public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void create(FileEntry lastEntry, String path) {
 | 
			
		||||
        var lastIndex = 0;
 | 
			
		||||
        while (lastIndex < path.length()) {
 | 
			
		||||
            var nextIndex = path.indexOf('/', lastIndex);
 | 
			
		||||
            if (nextIndex < 0) nextIndex = path.length();
 | 
			
		||||
 | 
			
		||||
            var part = path.substring(lastIndex, nextIndex);
 | 
			
		||||
            if (lastEntry.children == null) lastEntry.children = new HashMap<>();
 | 
			
		||||
 | 
			
		||||
            var nextEntry = lastEntry.children.get(part);
 | 
			
		||||
            if (nextEntry == null) {
 | 
			
		||||
                ResourceLocation childPath;
 | 
			
		||||
                try {
 | 
			
		||||
                    childPath = new ResourceLocation(namespace, subPath + "/" + path);
 | 
			
		||||
                } catch (ResourceLocationException e) {
 | 
			
		||||
                    LOG.warn("Cannot create resource location for {} ({})", part, e.getMessage());
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                lastEntry.children.put(part, nextEntry = new FileEntry(path, childPath));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            lastEntry = nextEntry;
 | 
			
		||||
            lastIndex = nextIndex + 1;
 | 
			
		||||
        }
 | 
			
		||||
    private FileEntry createEntry(String path) {
 | 
			
		||||
        return new FileEntry(new ResourceLocation(namespace, subPath + "/" + path));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public long getSize(FileEntry file) {
 | 
			
		||||
    protected byte[] getFileContents(String path, FileEntry file) throws IOException {
 | 
			
		||||
        var resource = manager.getResource(file.identifier).orElse(null);
 | 
			
		||||
        if (resource == null) return 0;
 | 
			
		||||
 | 
			
		||||
        try (var stream = resource.open()) {
 | 
			
		||||
            int total = 0, read = 0;
 | 
			
		||||
            do {
 | 
			
		||||
                total += read;
 | 
			
		||||
                read = stream.read(TEMP_BUFFER);
 | 
			
		||||
            } while (read > 0);
 | 
			
		||||
 | 
			
		||||
            return total;
 | 
			
		||||
        } catch (IOException e) {
 | 
			
		||||
            return 0;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public byte[] getContents(FileEntry file) throws IOException {
 | 
			
		||||
        var resource = manager.getResource(file.identifier).orElse(null);
 | 
			
		||||
        if (resource == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
 | 
			
		||||
        if (resource == null) throw new FileOperationException(path, NO_SUCH_FILE);
 | 
			
		||||
 | 
			
		||||
        try (var stream = resource.open()) {
 | 
			
		||||
            return stream.readAllBytes();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected static class FileEntry extends ArchiveMount.FileEntry<FileEntry> {
 | 
			
		||||
    protected static final class FileEntry extends ArchiveMount.FileEntry<FileEntry> {
 | 
			
		||||
        final ResourceLocation identifier;
 | 
			
		||||
 | 
			
		||||
        FileEntry(String path, ResourceLocation identifier) {
 | 
			
		||||
            super(path);
 | 
			
		||||
        FileEntry(ResourceLocation identifier) {
 | 
			
		||||
            this.identifier = identifier;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -64,13 +64,7 @@ public abstract class AbstractComputerItem extends BlockItem implements ICompute
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public @Nullable Mount createDataMount(ItemStack stack, ServerLevel level) {
 | 
			
		||||
        var family = getFamily();
 | 
			
		||||
        if (family != ComputerFamily.COMMAND) {
 | 
			
		||||
            var id = getComputerID(stack);
 | 
			
		||||
            if (id >= 0) {
 | 
			
		||||
                return ComputerCraftAPI.createSaveDirMount(level.getServer(), "computer/" + id, Config.computerSpaceLimit);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return null;
 | 
			
		||||
        var id = getComputerID(stack);
 | 
			
		||||
        return id >= 0 ? ComputerCraftAPI.createSaveDirMount(level.getServer(), "computer/" + id, Config.computerSpaceLimit) : null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,38 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.computer.items;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.filesystem.Mount;
 | 
			
		||||
import dan200.computercraft.shared.computer.blocks.ComputerBlock;
 | 
			
		||||
import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.context.BlockPlaceContext;
 | 
			
		||||
import net.minecraft.world.level.block.state.BlockState;
 | 
			
		||||
import org.jetbrains.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A {@link ComputerItem} which prevents players placing it without permission.
 | 
			
		||||
 *
 | 
			
		||||
 * @see net.minecraft.world.item.GameMasterBlockItem
 | 
			
		||||
 * @see dan200.computercraft.shared.computer.blocks.CommandComputerBlock
 | 
			
		||||
 */
 | 
			
		||||
public class CommandComputerItem extends ComputerItem {
 | 
			
		||||
    public CommandComputerItem(ComputerBlock<?> block, Properties settings) {
 | 
			
		||||
        super(block, settings);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected @Nullable BlockState getPlacementState(BlockPlaceContext context) {
 | 
			
		||||
        // Prohibit players placing this block in survival or when not opped.
 | 
			
		||||
        var player = context.getPlayer();
 | 
			
		||||
        return player != null && !player.canUseGameMasterBlocks() ? null : super.getPlacementState(context);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public @Nullable Mount createDataMount(ItemStack stack, ServerLevel level) {
 | 
			
		||||
        // Don't allow command computers to be mounted in disk drives.
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,7 +4,12 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.computer.menu;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.computer.upload.*;
 | 
			
		||||
import dan200.computercraft.core.apis.handles.ByteBufferChannel;
 | 
			
		||||
import dan200.computercraft.core.apis.transfer.TransferredFile;
 | 
			
		||||
import dan200.computercraft.core.apis.transfer.TransferredFiles;
 | 
			
		||||
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 it.unimi.dsi.fastutil.ints.IntOpenHashSet;
 | 
			
		||||
@@ -18,7 +23,6 @@ import org.slf4j.LoggerFactory;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The default concrete implementation of {@link ServerInputHandler}.
 | 
			
		||||
@@ -150,8 +154,14 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        computer.queueEvent("file_transfer", new Object[]{
 | 
			
		||||
            new TransferredFiles(player, owner, toUpload.stream().map(x -> new TransferredFile(x.getName(), x.getBytes())).collect(Collectors.toList())),
 | 
			
		||||
        computer.queueEvent(TransferredFiles.EVENT, new Object[]{
 | 
			
		||||
            new TransferredFiles(
 | 
			
		||||
                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);
 | 
			
		||||
                    }
 | 
			
		||||
                }),
 | 
			
		||||
        });
 | 
			
		||||
        return UploadResultMessage.queued(owner);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -141,7 +141,7 @@ public final class ComputerMBean implements DynamicMBean, ComputerMetricsObserve
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static class Counter {
 | 
			
		||||
    private static final class Counter {
 | 
			
		||||
        final AtomicLong value = new AtomicLong();
 | 
			
		||||
        final AtomicLong count = new AtomicLong();
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -5,31 +5,20 @@
 | 
			
		||||
package dan200.computercraft.shared.computer.recipe;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.computer.items.IComputerItem;
 | 
			
		||||
import net.minecraft.core.NonNullList;
 | 
			
		||||
import dan200.computercraft.shared.recipe.CustomShapedRecipe;
 | 
			
		||||
import dan200.computercraft.shared.recipe.ShapedRecipeSpec;
 | 
			
		||||
import net.minecraft.core.RegistryAccess;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.inventory.CraftingContainer;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.crafting.CraftingBookCategory;
 | 
			
		||||
import net.minecraft.world.item.crafting.Ingredient;
 | 
			
		||||
import net.minecraft.world.item.crafting.ShapedRecipe;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Represents a recipe which converts a computer from one form into another.
 | 
			
		||||
 * A recipe which converts a computer from one form into another.
 | 
			
		||||
 */
 | 
			
		||||
public abstract class ComputerConvertRecipe extends ShapedRecipe {
 | 
			
		||||
    private final String group;
 | 
			
		||||
    private final ItemStack result;
 | 
			
		||||
 | 
			
		||||
    public ComputerConvertRecipe(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result) {
 | 
			
		||||
        super(identifier, group, category, width, height, ingredients, result);
 | 
			
		||||
        this.group = group;
 | 
			
		||||
        this.result = result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ItemStack getResultItem() {
 | 
			
		||||
        return result;
 | 
			
		||||
public abstract class ComputerConvertRecipe extends CustomShapedRecipe {
 | 
			
		||||
    public ComputerConvertRecipe(ResourceLocation identifier, ShapedRecipeSpec recipe) {
 | 
			
		||||
        super(identifier, recipe);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected abstract ItemStack convert(IComputerItem item, ItemStack stack);
 | 
			
		||||
@@ -55,9 +44,4 @@ public abstract class ComputerConvertRecipe extends ShapedRecipe {
 | 
			
		||||
 | 
			
		||||
        return ItemStack.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getGroup() {
 | 
			
		||||
        return group;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,72 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.computer.recipe;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import dan200.computercraft.shared.util.RecipeUtil;
 | 
			
		||||
import net.minecraft.core.NonNullList;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.util.GsonHelper;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.crafting.CraftingBookCategory;
 | 
			
		||||
import net.minecraft.world.item.crafting.Ingredient;
 | 
			
		||||
import net.minecraft.world.item.crafting.RecipeSerializer;
 | 
			
		||||
 | 
			
		||||
public abstract class ComputerFamilyRecipe extends ComputerConvertRecipe {
 | 
			
		||||
    private final ComputerFamily family;
 | 
			
		||||
 | 
			
		||||
    public ComputerFamilyRecipe(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family) {
 | 
			
		||||
        super(identifier, group, category, width, height, ingredients, result);
 | 
			
		||||
        this.family = family;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ComputerFamily getFamily() {
 | 
			
		||||
        return family;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public abstract static class Serializer<T extends ComputerFamilyRecipe> implements RecipeSerializer<T> {
 | 
			
		||||
        protected abstract T create(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family);
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public T fromJson(ResourceLocation identifier, JsonObject json) {
 | 
			
		||||
            var group = GsonHelper.getAsString(json, "group", "");
 | 
			
		||||
            var category = CraftingBookCategory.CODEC.byName(GsonHelper.getAsString(json, "category", null), CraftingBookCategory.MISC);
 | 
			
		||||
            var family = RecipeUtil.getFamily(json, "family");
 | 
			
		||||
 | 
			
		||||
            var template = RecipeUtil.getTemplate(json);
 | 
			
		||||
            var result = itemStackFromJson(GsonHelper.getAsJsonObject(json, "result"));
 | 
			
		||||
 | 
			
		||||
            return create(identifier, group, category, template.width(), template.height(), template.ingredients(), result, family);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public T fromNetwork(ResourceLocation identifier, FriendlyByteBuf buf) {
 | 
			
		||||
            var width = buf.readVarInt();
 | 
			
		||||
            var height = buf.readVarInt();
 | 
			
		||||
            var group = buf.readUtf();
 | 
			
		||||
            var category = buf.readEnum(CraftingBookCategory.class);
 | 
			
		||||
 | 
			
		||||
            var ingredients = NonNullList.withSize(width * height, Ingredient.EMPTY);
 | 
			
		||||
            for (var i = 0; i < ingredients.size(); i++) ingredients.set(i, Ingredient.fromNetwork(buf));
 | 
			
		||||
 | 
			
		||||
            var result = buf.readItem();
 | 
			
		||||
            var family = buf.readEnum(ComputerFamily.class);
 | 
			
		||||
            return create(identifier, group, category, width, height, ingredients, result, family);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void toNetwork(FriendlyByteBuf buf, T recipe) {
 | 
			
		||||
            buf.writeVarInt(recipe.getWidth());
 | 
			
		||||
            buf.writeVarInt(recipe.getHeight());
 | 
			
		||||
            buf.writeUtf(recipe.getGroup());
 | 
			
		||||
            buf.writeEnum(recipe.category());
 | 
			
		||||
            for (var ingredient : recipe.getIngredients()) ingredient.toNetwork(buf);
 | 
			
		||||
            buf.writeItem(recipe.getResultItem());
 | 
			
		||||
            buf.writeEnum(recipe.getFamily());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,35 +4,57 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.computer.recipe;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import dan200.computercraft.shared.computer.items.IComputerItem;
 | 
			
		||||
import net.minecraft.core.NonNullList;
 | 
			
		||||
import dan200.computercraft.shared.recipe.ShapedRecipeSpec;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.crafting.CraftingBookCategory;
 | 
			
		||||
import net.minecraft.world.item.crafting.Ingredient;
 | 
			
		||||
import net.minecraft.world.item.crafting.RecipeSerializer;
 | 
			
		||||
 | 
			
		||||
public final class ComputerUpgradeRecipe extends ComputerFamilyRecipe {
 | 
			
		||||
    private ComputerUpgradeRecipe(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family) {
 | 
			
		||||
        super(identifier, group, category, width, height, ingredients, result, family);
 | 
			
		||||
/**
 | 
			
		||||
 * A recipe which "upgrades" a {@linkplain IComputerItem computer}, converting it from one {@linkplain ComputerFamily
 | 
			
		||||
 * family} to another.
 | 
			
		||||
 */
 | 
			
		||||
public final class ComputerUpgradeRecipe extends ComputerConvertRecipe {
 | 
			
		||||
    private final ComputerFamily family;
 | 
			
		||||
 | 
			
		||||
    private ComputerUpgradeRecipe(ResourceLocation identifier, ShapedRecipeSpec recipe, ComputerFamily family) {
 | 
			
		||||
        super(identifier, recipe);
 | 
			
		||||
        this.family = family;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected ItemStack convert(IComputerItem item, ItemStack stack) {
 | 
			
		||||
        return item.withFamily(stack, getFamily());
 | 
			
		||||
        return item.withFamily(stack, family);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public RecipeSerializer<?> getSerializer() {
 | 
			
		||||
    public RecipeSerializer<ComputerUpgradeRecipe> getSerializer() {
 | 
			
		||||
        return ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static class Serializer extends ComputerFamilyRecipe.Serializer<ComputerUpgradeRecipe> {
 | 
			
		||||
    public static class Serializer implements RecipeSerializer<ComputerUpgradeRecipe> {
 | 
			
		||||
        @Override
 | 
			
		||||
        protected ComputerUpgradeRecipe create(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family) {
 | 
			
		||||
            return new ComputerUpgradeRecipe(identifier, group, category, width, height, ingredients, result, family);
 | 
			
		||||
        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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -61,7 +61,7 @@ public class ItemDetails {
 | 
			
		||||
 | 
			
		||||
        /*
 | 
			
		||||
         * Used to hide some data from ItemStack tooltip.
 | 
			
		||||
         * @see https://minecraft.gamepedia.com/Tutorials/Command_NBT_tags
 | 
			
		||||
         * @see https://minecraft.wiki/w/Tutorials/Command_NBT_tags
 | 
			
		||||
         * @see ItemStack#getTooltip
 | 
			
		||||
         */
 | 
			
		||||
        var hideFlags = tag != null ? tag.getInt("HideFlags") : 0;
 | 
			
		||||
@@ -116,6 +116,7 @@ public class ItemDetails {
 | 
			
		||||
     * @param enchants    The enchantment map to add it to.
 | 
			
		||||
     * @see EnchantmentHelper
 | 
			
		||||
     */
 | 
			
		||||
    @SuppressWarnings("NonApiType")
 | 
			
		||||
    private static void addEnchantments(ListTag rawEnchants, ArrayList<Map<String, Object>> enchants) {
 | 
			
		||||
        if (rawEnchants.isEmpty()) return;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -69,7 +69,7 @@ public abstract class PermissionRegistry {
 | 
			
		||||
            .orElseGet(DefaultPermissionRegistry::new);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static class DefaultPermissionRegistry extends PermissionRegistry {
 | 
			
		||||
    private static final class DefaultPermissionRegistry extends PermissionRegistry {
 | 
			
		||||
        @Override
 | 
			
		||||
        public Predicate<CommandSourceStack> registerCommand(String command, UserLevel fallback) {
 | 
			
		||||
            checkNotFrozen();
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ import java.util.concurrent.atomic.AtomicReference;
 | 
			
		||||
public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
 | 
			
		||||
    private static final String NBT_ITEM = "Item";
 | 
			
		||||
 | 
			
		||||
    private static class MountInfo {
 | 
			
		||||
    private static final class MountInfo {
 | 
			
		||||
        @Nullable
 | 
			
		||||
        String mountPath;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -36,7 +36,7 @@ import java.util.Collections;
 | 
			
		||||
public class CableBlockEntity extends BlockEntity {
 | 
			
		||||
    private static final String NBT_PERIPHERAL_ENABLED = "PeripheralAccess";
 | 
			
		||||
 | 
			
		||||
    private class CableElement extends WiredModemElement {
 | 
			
		||||
    private final class CableElement extends WiredModemElement {
 | 
			
		||||
        @Override
 | 
			
		||||
        public Level getLevel() {
 | 
			
		||||
            return CableBlockEntity.this.getLevel();
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,8 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.peripheral.modem.wired;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftTags;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ServerContext;
 | 
			
		||||
import dan200.computercraft.shared.platform.ComponentAccess;
 | 
			
		||||
import dan200.computercraft.shared.platform.PlatformHelper;
 | 
			
		||||
@@ -124,8 +124,7 @@ public final class WiredModemLocalPeripheral {
 | 
			
		||||
    private IPeripheral getPeripheralFrom(Level world, BlockPos pos, Direction direction) {
 | 
			
		||||
        var offset = pos.relative(direction);
 | 
			
		||||
 | 
			
		||||
        var block = world.getBlockState(offset).getBlock();
 | 
			
		||||
        if (block == ModRegistry.Blocks.WIRED_MODEM_FULL.get() || block == ModRegistry.Blocks.CABLE.get()) return null;
 | 
			
		||||
        if (world.getBlockState(offset).is(ComputerCraftTags.Blocks.PERIPHERAL_HUB_IGNORE)) return null;
 | 
			
		||||
 | 
			
		||||
        var peripheral = peripherals.get((ServerLevel) world, pos, direction);
 | 
			
		||||
        return peripheral instanceof WiredModemPeripheral ? null : peripheral;
 | 
			
		||||
 
 | 
			
		||||
@@ -349,13 +349,15 @@ public class MonitorBlockEntity extends BlockEntity {
 | 
			
		||||
        // Either delete the current monitor or sync a new one.
 | 
			
		||||
        if (needsTerminal) {
 | 
			
		||||
            if (serverMonitor == null) serverMonitor = new ServerMonitor(advanced, this);
 | 
			
		||||
        } else {
 | 
			
		||||
            serverMonitor = null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Update the terminal's width and height and rebuild it. This ensures the monitor
 | 
			
		||||
        // is consistent when syncing it to other monitors.
 | 
			
		||||
        if (serverMonitor != null) serverMonitor.rebuild();
 | 
			
		||||
            // Update the terminal's width and height and rebuild it. This ensures the monitor
 | 
			
		||||
            // is consistent when syncing it to other monitors.
 | 
			
		||||
            serverMonitor.rebuild();
 | 
			
		||||
        } else {
 | 
			
		||||
            // Remove the terminal from the serverMonitor, but keep it around - this ensures that we sync
 | 
			
		||||
            // the (now blank) monitor to the client.
 | 
			
		||||
            if (serverMonitor != null) serverMonitor.reset();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Update the other monitors, setting coordinates, dimensions and the server terminal
 | 
			
		||||
        var pos = getBlockPos();
 | 
			
		||||
 
 | 
			
		||||
@@ -55,6 +55,12 @@ public class ServerMonitor {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    synchronized void reset() {
 | 
			
		||||
        if (terminal == null) return;
 | 
			
		||||
        terminal = null;
 | 
			
		||||
        markChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void markChanged() {
 | 
			
		||||
        if (!changed.getAndSet(true)) TickScheduler.schedule(origin.tickToken);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -188,7 +188,7 @@ public abstract class SpeakerPeripheral implements IPeripheral {
 | 
			
		||||
     * {@literal false}.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * ### Valid instruments
 | 
			
		||||
     * The speaker supports [all of Minecraft's noteblock instruments](https://minecraft.fandom.com/wiki/Note_Block#Instruments).
 | 
			
		||||
     * The speaker supports [all of Minecraft's noteblock instruments](https://minecraft.wiki/w/Note_Block#Instruments).
 | 
			
		||||
     * These are:
 | 
			
		||||
     * <p>
 | 
			
		||||
     * {@code "harp"}, {@code "basedrum"}, {@code "snare"}, {@code "hat"}, {@code "bass"}, {@code "flute"},
 | 
			
		||||
@@ -228,7 +228,7 @@ public abstract class SpeakerPeripheral implements IPeripheral {
 | 
			
		||||
    /**
 | 
			
		||||
     * Plays a Minecraft sound through the speaker.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * This takes the [name of a Minecraft sound](https://minecraft.fandom.com/wiki/Sounds.json), such as
 | 
			
		||||
     * This takes the [name of a Minecraft sound](https://minecraft.wiki/w/Sounds.json), such as
 | 
			
		||||
     * {@code "minecraft:block.note_block.harp"}, as well as an optional volume and pitch.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * Only one sound can be played at once. This function will return {@literal false} if another sound was started
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,86 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.recipe;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import com.google.gson.JsonParseException;
 | 
			
		||||
import com.mojang.serialization.DataResult;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import net.minecraft.Util;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.crafting.RecipeSerializer;
 | 
			
		||||
import net.minecraft.world.item.crafting.ShapedRecipe;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A custom version of {@link ShapedRecipe}, which can be converted to and from a {@link ShapedRecipeSpec}.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This recipe may both be used as a normal recipe (behaving mostly the same as {@link ShapedRecipe}, with
 | 
			
		||||
 * {@linkplain RecipeUtil#itemStackFromJson(JsonObject) support for putting nbt on the result}), or subclassed to
 | 
			
		||||
 * customise the crafting behaviour.
 | 
			
		||||
 */
 | 
			
		||||
public class CustomShapedRecipe extends ShapedRecipe {
 | 
			
		||||
    private final ItemStack result;
 | 
			
		||||
 | 
			
		||||
    public CustomShapedRecipe(ResourceLocation id, ShapedRecipeSpec recipe) {
 | 
			
		||||
        super(
 | 
			
		||||
            id,
 | 
			
		||||
            recipe.properties().group(), recipe.properties().category(),
 | 
			
		||||
            recipe.template().width(), recipe.template().height(), recipe.template().ingredients(),
 | 
			
		||||
            recipe.result()
 | 
			
		||||
        );
 | 
			
		||||
        this.result = recipe.result();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public final ShapedRecipeSpec toSpec() {
 | 
			
		||||
        return new ShapedRecipeSpec(RecipeProperties.of(this), ShapedTemplate.of(this), result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public RecipeSerializer<? extends CustomShapedRecipe> getSerializer() {
 | 
			
		||||
        return ModRegistry.RecipeSerializers.SHAPED.get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public interface Factory<R> {
 | 
			
		||||
        R create(ResourceLocation id, ShapedRecipeSpec recipe);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static <T extends CustomShapedRecipe> RecipeSerializer<T> serialiser(CustomShapedRecipe.Factory<T> factory) {
 | 
			
		||||
        return new Serialiser<>((id, r) -> DataResult.success(factory.create(id, r)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static <T extends CustomShapedRecipe> RecipeSerializer<T> validatingSerialiser(CustomShapedRecipe.Factory<DataResult<T>> factory) {
 | 
			
		||||
        return new Serialiser<>(factory);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private record Serialiser<T extends CustomShapedRecipe>(
 | 
			
		||||
        Factory<DataResult<T>> factory
 | 
			
		||||
    ) implements RecipeSerializer<T> {
 | 
			
		||||
        private Serialiser(Factory<DataResult<T>> factory) {
 | 
			
		||||
            this.factory = (id, r) -> factory.create(id, r).flatMap(x -> {
 | 
			
		||||
                if (x.getSerializer() != this) {
 | 
			
		||||
                    return DataResult.error(() -> "Expected serialiser to be " + this + ", but was " + x.getSerializer());
 | 
			
		||||
                }
 | 
			
		||||
                return DataResult.success(x);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public T fromJson(ResourceLocation id, JsonObject json) {
 | 
			
		||||
            return Util.getOrThrow(factory.create(id, ShapedRecipeSpec.fromJson(json)), JsonParseException::new);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public T fromNetwork(ResourceLocation id, FriendlyByteBuf buffer) {
 | 
			
		||||
            return Util.getOrThrow(factory.create(id, ShapedRecipeSpec.fromNetwork(buffer)), IllegalStateException::new);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void toNetwork(FriendlyByteBuf buffer, T recipe) {
 | 
			
		||||
            recipe.toSpec().toNetwork(buffer);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,81 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.recipe;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import com.google.gson.JsonParseException;
 | 
			
		||||
import com.mojang.serialization.DataResult;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import net.minecraft.Util;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.crafting.RecipeSerializer;
 | 
			
		||||
import net.minecraft.world.item.crafting.ShapelessRecipe;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A custom version of {@link ShapelessRecipe}, which can be converted to and from a {@link ShapelessRecipeSpec}.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This recipe may both be used as a normal recipe (behaving mostly the same as {@link ShapelessRecipe}, with
 | 
			
		||||
 * {@linkplain RecipeUtil#itemStackFromJson(JsonObject) support for putting nbt on the result}), or subclassed to
 | 
			
		||||
 * customise the crafting behaviour.
 | 
			
		||||
 */
 | 
			
		||||
public class CustomShapelessRecipe extends ShapelessRecipe {
 | 
			
		||||
    private final ItemStack result;
 | 
			
		||||
 | 
			
		||||
    public CustomShapelessRecipe(ResourceLocation id, ShapelessRecipeSpec recipe) {
 | 
			
		||||
        super(id, recipe.properties().group(), recipe.properties().category(), recipe.result(), recipe.ingredients());
 | 
			
		||||
        this.result = recipe.result();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public final ShapelessRecipeSpec toSpec() {
 | 
			
		||||
        return new ShapelessRecipeSpec(RecipeProperties.of(this), getIngredients(), result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public RecipeSerializer<? extends CustomShapelessRecipe> getSerializer() {
 | 
			
		||||
        return ModRegistry.RecipeSerializers.SHAPELESS.get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public interface Factory<R> {
 | 
			
		||||
        R create(ResourceLocation id, ShapelessRecipeSpec recipe);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static <T extends CustomShapelessRecipe> RecipeSerializer<T> serialiser(Factory<T> factory) {
 | 
			
		||||
        return new CustomShapelessRecipe.Serialiser<>((id, r) -> DataResult.success(factory.create(id, r)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static <T extends CustomShapelessRecipe> RecipeSerializer<T> validatingSerialiser(Factory<DataResult<T>> factory) {
 | 
			
		||||
        return new CustomShapelessRecipe.Serialiser<>(factory);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private record Serialiser<T extends CustomShapelessRecipe>(
 | 
			
		||||
        Factory<DataResult<T>> factory
 | 
			
		||||
    ) implements RecipeSerializer<T> {
 | 
			
		||||
        private Serialiser(Factory<DataResult<T>> factory) {
 | 
			
		||||
            this.factory = (id, r) -> factory.create(id, r).flatMap(x -> {
 | 
			
		||||
                if (x.getSerializer() != this) {
 | 
			
		||||
                    return DataResult.error(() -> "Expected serialiser to be " + this + ", but was " + x.getSerializer());
 | 
			
		||||
                }
 | 
			
		||||
                return DataResult.success(x);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public T fromJson(ResourceLocation id, JsonObject json) {
 | 
			
		||||
            return Util.getOrThrow(factory.create(id, ShapelessRecipeSpec.fromJson(json)), JsonParseException::new);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public T fromNetwork(ResourceLocation id, FriendlyByteBuf buffer) {
 | 
			
		||||
            return Util.getOrThrow(factory.create(id, ShapelessRecipeSpec.fromNetwork(buffer)), IllegalStateException::new);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void toNetwork(FriendlyByteBuf buffer, T recipe) {
 | 
			
		||||
            recipe.toSpec().toNetwork(buffer);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,41 @@
 | 
			
		||||
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: LicenseRef-CCPL
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.recipe;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import net.minecraft.core.RegistryAccess;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.inventory.CraftingContainer;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.crafting.CustomRecipe;
 | 
			
		||||
import net.minecraft.world.item.crafting.RecipeSerializer;
 | 
			
		||||
import net.minecraft.world.item.crafting.ShapedRecipe;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A fake {@link ShapedRecipe}, which appears in the recipe book (and other recipe mods), but cannot be crafted.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This is used to represent examples for our {@link CustomRecipe}s.
 | 
			
		||||
 */
 | 
			
		||||
public final class ImpostorShapedRecipe extends CustomShapedRecipe {
 | 
			
		||||
    public ImpostorShapedRecipe(ResourceLocation id, ShapedRecipeSpec recipe) {
 | 
			
		||||
        super(id, recipe);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean matches(CraftingContainer inv, Level world) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public ItemStack assemble(CraftingContainer inventory, RegistryAccess registryAccess) {
 | 
			
		||||
        return ItemStack.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public RecipeSerializer<ImpostorShapedRecipe> getSerializer() {
 | 
			
		||||
        return ModRegistry.RecipeSerializers.IMPOSTOR_SHAPED.get();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,41 @@
 | 
			
		||||
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: LicenseRef-CCPL
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.recipe;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import net.minecraft.core.RegistryAccess;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.inventory.CraftingContainer;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.crafting.CustomRecipe;
 | 
			
		||||
import net.minecraft.world.item.crafting.RecipeSerializer;
 | 
			
		||||
import net.minecraft.world.item.crafting.ShapelessRecipe;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A fake {@link ShapelessRecipe}, which appears in the recipe book (and other recipe mods), but cannot be crafted.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This is used to represent examples for our {@link CustomRecipe}s.
 | 
			
		||||
 */
 | 
			
		||||
public final class ImpostorShapelessRecipe extends CustomShapelessRecipe {
 | 
			
		||||
    public ImpostorShapelessRecipe(ResourceLocation id, ShapelessRecipeSpec recipe) {
 | 
			
		||||
        super(id, recipe);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean matches(CraftingContainer inv, Level world) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public ItemStack assemble(CraftingContainer inventory, RegistryAccess access) {
 | 
			
		||||
        return ItemStack.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public RecipeSerializer<ImpostorShapelessRecipe> getSerializer() {
 | 
			
		||||
        return ModRegistry.RecipeSerializers.IMPOSTOR_SHAPELESS.get();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,33 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.recipe;
 | 
			
		||||
 | 
			
		||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
 | 
			
		||||
import com.mojang.datafixers.util.Either;
 | 
			
		||||
import com.mojang.serialization.Codec;
 | 
			
		||||
import com.mojang.serialization.DataResult;
 | 
			
		||||
import net.minecraft.nbt.CompoundTag;
 | 
			
		||||
import net.minecraft.nbt.TagParser;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Additional codecs for working with recipes.
 | 
			
		||||
 */
 | 
			
		||||
public class MoreCodecs {
 | 
			
		||||
    /**
 | 
			
		||||
     * A codec for {@link CompoundTag}s, which either accepts a NBT-string or a JSON object.
 | 
			
		||||
     */
 | 
			
		||||
    public static final Codec<CompoundTag> TAG = Codec.either(Codec.STRING, CompoundTag.CODEC).flatXmap(
 | 
			
		||||
        either -> either.map(MoreCodecs::parseTag, DataResult::success),
 | 
			
		||||
        nbtCompound -> DataResult.success(Either.left(nbtCompound.getAsString()))
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    private static DataResult<CompoundTag> parseTag(String contents) {
 | 
			
		||||
        try {
 | 
			
		||||
            return DataResult.success(TagParser.parseTag(contents));
 | 
			
		||||
        } catch (CommandSyntaxException e) {
 | 
			
		||||
            return DataResult.error(e::getMessage);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,40 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.recipe;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.util.GsonHelper;
 | 
			
		||||
import net.minecraft.world.item.crafting.CraftingBookCategory;
 | 
			
		||||
import net.minecraft.world.item.crafting.CraftingRecipe;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Common properties that appear in all {@link CraftingRecipe}s.
 | 
			
		||||
 *
 | 
			
		||||
 * @param group    The (optional) group of the recipe, see {@link CraftingRecipe#getGroup()}.
 | 
			
		||||
 * @param category The category the recipe appears in, see {@link CraftingRecipe#category()}.
 | 
			
		||||
 */
 | 
			
		||||
public record RecipeProperties(String group, CraftingBookCategory category) {
 | 
			
		||||
    public static RecipeProperties of(CraftingRecipe recipe) {
 | 
			
		||||
        return new RecipeProperties(recipe.getGroup(), recipe.category());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static RecipeProperties fromJson(JsonObject json) {
 | 
			
		||||
        var group = GsonHelper.getAsString(json, "group", "");
 | 
			
		||||
        var category = CraftingBookCategory.CODEC.byName(GsonHelper.getAsString(json, "category", null), CraftingBookCategory.MISC);
 | 
			
		||||
        return new RecipeProperties(group, category);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static RecipeProperties fromNetwork(FriendlyByteBuf buffer) {
 | 
			
		||||
        var group = buffer.readUtf();
 | 
			
		||||
        var category = buffer.readEnum(CraftingBookCategory.class);
 | 
			
		||||
        return new RecipeProperties(group, category);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void toNetwork(FriendlyByteBuf buffer) {
 | 
			
		||||
        buffer.writeUtf(group());
 | 
			
		||||
        buffer.writeEnum(category());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,73 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.recipe;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import com.google.gson.JsonParseException;
 | 
			
		||||
import com.google.gson.JsonSyntaxException;
 | 
			
		||||
import com.mojang.serialization.JsonOps;
 | 
			
		||||
import net.minecraft.Util;
 | 
			
		||||
import net.minecraft.core.NonNullList;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.util.GsonHelper;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.crafting.Ingredient;
 | 
			
		||||
import net.minecraft.world.item.crafting.ShapedRecipe;
 | 
			
		||||
 | 
			
		||||
public final class RecipeUtil {
 | 
			
		||||
    private RecipeUtil() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static NonNullList<Ingredient> readIngredients(FriendlyByteBuf buffer) {
 | 
			
		||||
        var count = buffer.readVarInt();
 | 
			
		||||
        var ingredients = NonNullList.withSize(count, Ingredient.EMPTY);
 | 
			
		||||
        for (var i = 0; i < ingredients.size(); i++) ingredients.set(i, Ingredient.fromNetwork(buffer));
 | 
			
		||||
        return ingredients;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void writeIngredients(FriendlyByteBuf buffer, NonNullList<Ingredient> ingredients) {
 | 
			
		||||
        buffer.writeCollection(ingredients, (a, b) -> b.toNetwork(a));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static NonNullList<Ingredient> readShapelessIngredients(JsonObject json) {
 | 
			
		||||
        NonNullList<Ingredient> ingredients = NonNullList.create();
 | 
			
		||||
 | 
			
		||||
        var ingredientsList = GsonHelper.getAsJsonArray(json, "ingredients");
 | 
			
		||||
        for (var i = 0; i < ingredientsList.size(); ++i) {
 | 
			
		||||
            var ingredient = Ingredient.fromJson(ingredientsList.get(i));
 | 
			
		||||
            if (!ingredient.isEmpty()) ingredients.add(ingredient);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (ingredients.isEmpty()) throw new JsonParseException("No ingredients for shapeless recipe");
 | 
			
		||||
        if (ingredients.size() > 9) {
 | 
			
		||||
            throw new JsonParseException("Too many ingredients for shapeless recipe the max is 9");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return ingredients;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Extends {@link ShapedRecipe#itemStackFromJson(JsonObject)} with support for the {@code nbt} field.
 | 
			
		||||
     *
 | 
			
		||||
     * @param json The json to extract the item from.
 | 
			
		||||
     * @return The parsed item stack.
 | 
			
		||||
     */
 | 
			
		||||
    public static ItemStack itemStackFromJson(JsonObject json) {
 | 
			
		||||
        var item = ShapedRecipe.itemFromJson(json);
 | 
			
		||||
        if (json.has("data")) throw new JsonParseException("Disallowed data tag found");
 | 
			
		||||
 | 
			
		||||
        var count = GsonHelper.getAsInt(json, "count", 1);
 | 
			
		||||
        if (count < 1) throw new JsonSyntaxException("Invalid output count: " + count);
 | 
			
		||||
 | 
			
		||||
        var stack = new ItemStack(item, count);
 | 
			
		||||
 | 
			
		||||
        var nbt = json.get("nbt");
 | 
			
		||||
        if (nbt != null) {
 | 
			
		||||
            stack.setTag(Util.getOrThrow(MoreCodecs.TAG.parse(JsonOps.INSTANCE, nbt), JsonParseException::new));
 | 
			
		||||
        }
 | 
			
		||||
        return stack;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,43 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.recipe;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.util.GsonHelper;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.crafting.ShapedRecipe;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A description of a {@link ShapedRecipe}.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This is meant to be used in conjunction with {@link CustomShapedRecipe} for more reusable serialisation and
 | 
			
		||||
 * deserialisation of {@link ShapedRecipe}-like recipes.
 | 
			
		||||
 *
 | 
			
		||||
 * @param properties The common properties of this recipe.
 | 
			
		||||
 * @param template   The shaped template of the recipe.
 | 
			
		||||
 * @param result     The result of the recipe.
 | 
			
		||||
 */
 | 
			
		||||
public record ShapedRecipeSpec(RecipeProperties properties, ShapedTemplate template, ItemStack result) {
 | 
			
		||||
    public static ShapedRecipeSpec fromJson(JsonObject json) {
 | 
			
		||||
        var properties = RecipeProperties.fromJson(json);
 | 
			
		||||
        var template = ShapedTemplate.fromJson(json);
 | 
			
		||||
        var result = RecipeUtil.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result"));
 | 
			
		||||
        return new ShapedRecipeSpec(properties, template, result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static ShapedRecipeSpec fromNetwork(FriendlyByteBuf buffer) {
 | 
			
		||||
        var properties = RecipeProperties.fromNetwork(buffer);
 | 
			
		||||
        var template = ShapedTemplate.fromNetwork(buffer);
 | 
			
		||||
        var result = buffer.readItem();
 | 
			
		||||
        return new ShapedRecipeSpec(properties, template, result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void toNetwork(FriendlyByteBuf buffer) {
 | 
			
		||||
        properties().toNetwork(buffer);
 | 
			
		||||
        template().toNetwork(buffer);
 | 
			
		||||
        buffer.writeItem(result());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,99 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.recipe;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import com.google.gson.JsonSyntaxException;
 | 
			
		||||
import net.minecraft.core.NonNullList;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.util.GsonHelper;
 | 
			
		||||
import net.minecraft.world.item.crafting.Ingredient;
 | 
			
		||||
import net.minecraft.world.item.crafting.ShapedRecipe;
 | 
			
		||||
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.HashSet;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The template for {@linkplain ShapedRecipe shaped recipes}. This largely exists for parsing shaped recipes from JSON.
 | 
			
		||||
 *
 | 
			
		||||
 * @param width       The width of the recipe, see {@link ShapedRecipe#getWidth()}.
 | 
			
		||||
 * @param height      The height of the recipe, see {@link ShapedRecipe#getHeight()}.
 | 
			
		||||
 * @param ingredients The ingredients in the recipe, see {@link ShapedRecipe#getIngredients()}
 | 
			
		||||
 */
 | 
			
		||||
public record ShapedTemplate(int width, int height, NonNullList<Ingredient> ingredients) {
 | 
			
		||||
    public static ShapedTemplate of(ShapedRecipe recipe) {
 | 
			
		||||
        return new ShapedTemplate(recipe.getWidth(), recipe.getHeight(), recipe.getIngredients());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static ShapedTemplate fromJson(JsonObject json) {
 | 
			
		||||
        Map<Character, Ingredient> key = new HashMap<>();
 | 
			
		||||
        for (var entry : GsonHelper.getAsJsonObject(json, "key").entrySet()) {
 | 
			
		||||
            if (entry.getKey().length() != 1) {
 | 
			
		||||
                throw new JsonSyntaxException("Invalid key entry: '" + entry.getKey() + "' is an invalid symbol (must be 1 character only).");
 | 
			
		||||
            }
 | 
			
		||||
            if (" ".equals(entry.getKey())) {
 | 
			
		||||
                throw new JsonSyntaxException("Invalid key entry: ' ' is a reserved symbol.");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            key.put(entry.getKey().charAt(0), Ingredient.fromJson(entry.getValue()));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var patternList = GsonHelper.getAsJsonArray(json, "pattern");
 | 
			
		||||
        if (patternList.size() == 0) {
 | 
			
		||||
            throw new JsonSyntaxException("Invalid pattern: empty pattern not allowed");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var pattern = new String[patternList.size()];
 | 
			
		||||
        for (var x = 0; x < pattern.length; x++) {
 | 
			
		||||
            var line = GsonHelper.convertToString(patternList.get(x), "pattern[" + x + "]");
 | 
			
		||||
            if (x > 0 && pattern[0].length() != line.length()) {
 | 
			
		||||
                throw new JsonSyntaxException("Invalid pattern: each row must  be the same width");
 | 
			
		||||
            }
 | 
			
		||||
            pattern[x] = line;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var width = pattern[0].length();
 | 
			
		||||
        var height = pattern.length;
 | 
			
		||||
        var ingredients = NonNullList.withSize(width * height, Ingredient.EMPTY);
 | 
			
		||||
 | 
			
		||||
        Set<Character> missingKeys = new HashSet<>(key.keySet());
 | 
			
		||||
 | 
			
		||||
        var ingredientIdx = 0;
 | 
			
		||||
        for (var line : pattern) {
 | 
			
		||||
            for (var x = 0; x < line.length(); x++) {
 | 
			
		||||
                var chr = line.charAt(x);
 | 
			
		||||
                var ing = chr == ' ' ? Ingredient.EMPTY : key.get(chr);
 | 
			
		||||
                if (ing == null) {
 | 
			
		||||
                    throw new JsonSyntaxException("Pattern references symbol '" + chr + "' but it's not defined in the key");
 | 
			
		||||
                }
 | 
			
		||||
                ingredients.set(ingredientIdx++, ing);
 | 
			
		||||
                missingKeys.remove(chr);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!missingKeys.isEmpty()) {
 | 
			
		||||
            throw new JsonSyntaxException("Key defines symbols that aren't used in pattern: " + missingKeys);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new ShapedTemplate(width, height, ingredients);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static ShapedTemplate fromNetwork(FriendlyByteBuf buffer) {
 | 
			
		||||
        var width = buffer.readVarInt();
 | 
			
		||||
        var height = buffer.readVarInt();
 | 
			
		||||
        var ingredients = NonNullList.withSize(width * height, Ingredient.EMPTY);
 | 
			
		||||
        for (var i = 0; i < ingredients.size(); ++i) ingredients.set(i, Ingredient.fromNetwork(buffer));
 | 
			
		||||
        return new ShapedTemplate(width, height, ingredients);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void toNetwork(FriendlyByteBuf buffer) {
 | 
			
		||||
        buffer.writeVarInt(width());
 | 
			
		||||
        buffer.writeVarInt(height());
 | 
			
		||||
        for (var ingredient : ingredients) ingredient.toNetwork(buffer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,46 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.recipe;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import net.minecraft.core.NonNullList;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.util.GsonHelper;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.crafting.Ingredient;
 | 
			
		||||
import net.minecraft.world.item.crafting.ShapelessRecipe;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A description of a {@link ShapelessRecipe}.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This is meant to be used in conjunction with {@link CustomShapelessRecipe} for more reusable serialisation and
 | 
			
		||||
 * deserialisation of {@link ShapelessRecipe}-like recipes.
 | 
			
		||||
 *
 | 
			
		||||
 * @param properties  The common properties of this recipe.
 | 
			
		||||
 * @param ingredients The ingredients of the recipe.
 | 
			
		||||
 * @param result      The result of the recipe.
 | 
			
		||||
 */
 | 
			
		||||
public record ShapelessRecipeSpec(RecipeProperties properties, NonNullList<Ingredient> ingredients, ItemStack result) {
 | 
			
		||||
    public static ShapelessRecipeSpec fromJson(JsonObject json) {
 | 
			
		||||
        var properties = RecipeProperties.fromJson(json);
 | 
			
		||||
        var ingredients = RecipeUtil.readShapelessIngredients(json);
 | 
			
		||||
        var result = RecipeUtil.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result"));
 | 
			
		||||
        return new ShapelessRecipeSpec(properties, ingredients, result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static ShapelessRecipeSpec fromNetwork(FriendlyByteBuf buffer) {
 | 
			
		||||
        var properties = RecipeProperties.fromNetwork(buffer);
 | 
			
		||||
        var ingredients = RecipeUtil.readIngredients(buffer);
 | 
			
		||||
        var result = buffer.readItem();
 | 
			
		||||
 | 
			
		||||
        return new ShapelessRecipeSpec(properties, ingredients, result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void toNetwork(FriendlyByteBuf buffer) {
 | 
			
		||||
        properties().toNetwork(buffer);
 | 
			
		||||
        RecipeUtil.writeIngredients(buffer, ingredients());
 | 
			
		||||
        buffer.writeItem(result());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -239,7 +239,7 @@ public class TurtlePlaceCommand implements TurtleCommand {
 | 
			
		||||
        world.sendBlockUpdated(tile.getBlockPos(), tile.getBlockState(), tile.getBlockState(), Block.UPDATE_ALL);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static class ErrorMessage {
 | 
			
		||||
    private static final class ErrorMessage {
 | 
			
		||||
        @Nullable
 | 
			
		||||
        String message;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,32 +4,30 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.turtle.recipes;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonArray;
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import com.google.gson.JsonParseException;
 | 
			
		||||
import dan200.computercraft.api.turtle.TurtleSide;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import dan200.computercraft.shared.recipe.CustomShapelessRecipe;
 | 
			
		||||
import dan200.computercraft.shared.recipe.ShapelessRecipeSpec;
 | 
			
		||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
 | 
			
		||||
import net.minecraft.core.NonNullList;
 | 
			
		||||
import net.minecraft.core.RegistryAccess;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.util.GsonHelper;
 | 
			
		||||
import net.minecraft.world.inventory.CraftingContainer;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.crafting.*;
 | 
			
		||||
import net.minecraft.world.item.crafting.RecipeSerializer;
 | 
			
		||||
import net.minecraft.world.item.crafting.ShapelessRecipe;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A {@link ShapelessRecipe} which sets the {@linkplain TurtleItem#getOverlay(ItemStack)} turtle's overlay} instead.
 | 
			
		||||
 */
 | 
			
		||||
public class TurtleOverlayRecipe extends ShapelessRecipe {
 | 
			
		||||
public class TurtleOverlayRecipe extends CustomShapelessRecipe {
 | 
			
		||||
    private final ResourceLocation overlay;
 | 
			
		||||
    private final ItemStack result;
 | 
			
		||||
 | 
			
		||||
    public TurtleOverlayRecipe(ResourceLocation id, String group, CraftingBookCategory category, ItemStack result, NonNullList<Ingredient> ingredients, ResourceLocation overlay) {
 | 
			
		||||
        super(id, group, category, result, ingredients);
 | 
			
		||||
    public TurtleOverlayRecipe(ResourceLocation id, ShapelessRecipeSpec spec, ResourceLocation overlay) {
 | 
			
		||||
        super(id, spec);
 | 
			
		||||
        this.overlay = overlay;
 | 
			
		||||
        this.result = result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static ItemStack make(ItemStack stack, ResourceLocation overlay) {
 | 
			
		||||
@@ -56,63 +54,29 @@ public class TurtleOverlayRecipe extends ShapelessRecipe {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public RecipeSerializer<?> getSerializer() {
 | 
			
		||||
    public RecipeSerializer<TurtleOverlayRecipe> getSerializer() {
 | 
			
		||||
        return ModRegistry.RecipeSerializers.TURTLE_OVERLAY.get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static class Serializer implements RecipeSerializer<TurtleOverlayRecipe> {
 | 
			
		||||
        @Override
 | 
			
		||||
        public TurtleOverlayRecipe fromJson(ResourceLocation id, JsonObject json) {
 | 
			
		||||
            var group = GsonHelper.getAsString(json, "group", "");
 | 
			
		||||
            var category = CraftingBookCategory.CODEC.byName(GsonHelper.getAsString(json, "category", null), CraftingBookCategory.MISC);
 | 
			
		||||
            var ingredients = readIngredients(GsonHelper.getAsJsonArray(json, "ingredients"));
 | 
			
		||||
 | 
			
		||||
            if (ingredients.isEmpty()) throw new JsonParseException("No ingredients for shapeless recipe");
 | 
			
		||||
            if (ingredients.size() > 9) {
 | 
			
		||||
                throw new JsonParseException("Too many ingredients for shapeless recipe the max is 9");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var recipe = ShapelessRecipeSpec.fromJson(json);
 | 
			
		||||
            var overlay = new ResourceLocation(GsonHelper.getAsString(json, "overlay"));
 | 
			
		||||
 | 
			
		||||
            // We could derive this from the ingredients, but we want to avoid evaluating the ingredients too early, so
 | 
			
		||||
            // it's easier to do this.
 | 
			
		||||
            var result = make(ShapedRecipe.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result")), overlay);
 | 
			
		||||
 | 
			
		||||
            return new TurtleOverlayRecipe(id, group, category, result, ingredients, overlay);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private NonNullList<Ingredient> readIngredients(JsonArray arrays) {
 | 
			
		||||
            NonNullList<Ingredient> items = NonNullList.create();
 | 
			
		||||
            for (var i = 0; i < arrays.size(); ++i) {
 | 
			
		||||
                var ingredient = Ingredient.fromJson(arrays.get(i));
 | 
			
		||||
                if (!ingredient.isEmpty()) items.add(ingredient);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return items;
 | 
			
		||||
            return new TurtleOverlayRecipe(id, recipe, overlay);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public TurtleOverlayRecipe fromNetwork(ResourceLocation id, FriendlyByteBuf buffer) {
 | 
			
		||||
            var group = buffer.readUtf();
 | 
			
		||||
            var category = buffer.readEnum(CraftingBookCategory.class);
 | 
			
		||||
            var count = buffer.readVarInt();
 | 
			
		||||
            var items = NonNullList.withSize(count, Ingredient.EMPTY);
 | 
			
		||||
 | 
			
		||||
            for (var j = 0; j < items.size(); j++) items.set(j, Ingredient.fromNetwork(buffer));
 | 
			
		||||
            var result = buffer.readItem();
 | 
			
		||||
            var recipe = ShapelessRecipeSpec.fromNetwork(buffer);
 | 
			
		||||
            var overlay = buffer.readResourceLocation();
 | 
			
		||||
 | 
			
		||||
            return new TurtleOverlayRecipe(id, group, category, result, items, overlay);
 | 
			
		||||
            return new TurtleOverlayRecipe(id, recipe, overlay);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void toNetwork(FriendlyByteBuf buffer, TurtleOverlayRecipe recipe) {
 | 
			
		||||
            buffer.writeUtf(recipe.getGroup());
 | 
			
		||||
            buffer.writeEnum(recipe.category());
 | 
			
		||||
            buffer.writeVarInt(recipe.getIngredients().size());
 | 
			
		||||
 | 
			
		||||
            for (var ingredient : recipe.getIngredients()) ingredient.toNetwork(buffer);
 | 
			
		||||
            buffer.writeItem(recipe.result);
 | 
			
		||||
            recipe.toSpec().toNetwork(buffer);
 | 
			
		||||
            buffer.writeResourceLocation(recipe.overlay);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,26 +4,33 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.turtle.recipes;
 | 
			
		||||
 | 
			
		||||
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.computer.recipe.ComputerFamilyRecipe;
 | 
			
		||||
import dan200.computercraft.shared.computer.recipe.ComputerConvertRecipe;
 | 
			
		||||
import dan200.computercraft.shared.recipe.ShapedRecipeSpec;
 | 
			
		||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
 | 
			
		||||
import net.minecraft.core.NonNullList;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.crafting.CraftingBookCategory;
 | 
			
		||||
import net.minecraft.world.item.crafting.Ingredient;
 | 
			
		||||
import net.minecraft.world.item.crafting.RecipeSerializer;
 | 
			
		||||
 | 
			
		||||
public final class TurtleRecipe extends ComputerFamilyRecipe {
 | 
			
		||||
    public TurtleRecipe(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family) {
 | 
			
		||||
        super(identifier, group, category, width, height, ingredients, result, family);
 | 
			
		||||
/**
 | 
			
		||||
 * The recipe which crafts a turtle from an existing computer item.
 | 
			
		||||
 */
 | 
			
		||||
public final class TurtleRecipe extends ComputerConvertRecipe {
 | 
			
		||||
    private final TurtleItem turtle;
 | 
			
		||||
 | 
			
		||||
    private TurtleRecipe(ResourceLocation id, ShapedRecipeSpec recipe, TurtleItem turtle) {
 | 
			
		||||
        super(id, recipe);
 | 
			
		||||
        this.turtle = turtle;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public RecipeSerializer<?> getSerializer() {
 | 
			
		||||
        return ModRegistry.RecipeSerializers.TURTLE.get();
 | 
			
		||||
    public static DataResult<TurtleRecipe> of(ResourceLocation id, ShapedRecipeSpec recipe) {
 | 
			
		||||
        if (!(recipe.result().getItem() instanceof TurtleItem turtle)) {
 | 
			
		||||
            return DataResult.error(() -> recipe.result().getItem() + " is not a turtle item");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return DataResult.success(new TurtleRecipe(id, recipe, turtle));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -31,13 +38,11 @@ public final class TurtleRecipe extends ComputerFamilyRecipe {
 | 
			
		||||
        var computerID = item.getComputerID(stack);
 | 
			
		||||
        var label = item.getLabel(stack);
 | 
			
		||||
 | 
			
		||||
        return TurtleItem.create(computerID, label, -1, getFamily(), null, null, 0, null);
 | 
			
		||||
        return turtle.create(computerID, label, -1, null, null, 0, null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static class Serializer extends ComputerFamilyRecipe.Serializer<TurtleRecipe> {
 | 
			
		||||
        @Override
 | 
			
		||||
        protected TurtleRecipe create(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family) {
 | 
			
		||||
            return new TurtleRecipe(identifier, group, category, width, height, ingredients, result, family);
 | 
			
		||||
        }
 | 
			
		||||
    @Override
 | 
			
		||||
    public RecipeSerializer<TurtleRecipe> getSerializer() {
 | 
			
		||||
        return ModRegistry.RecipeSerializers.TURTLE.get();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,88 +0,0 @@
 | 
			
		||||
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: LicenseRef-CCPL
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.util;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import net.minecraft.core.NonNullList;
 | 
			
		||||
import net.minecraft.core.RegistryAccess;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.util.GsonHelper;
 | 
			
		||||
import net.minecraft.world.inventory.CraftingContainer;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.crafting.CraftingBookCategory;
 | 
			
		||||
import net.minecraft.world.item.crafting.Ingredient;
 | 
			
		||||
import net.minecraft.world.item.crafting.RecipeSerializer;
 | 
			
		||||
import net.minecraft.world.item.crafting.ShapedRecipe;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
 | 
			
		||||
public final class ImpostorRecipe extends ShapedRecipe {
 | 
			
		||||
    private final String group;
 | 
			
		||||
    private final ItemStack result;
 | 
			
		||||
 | 
			
		||||
    private ImpostorRecipe(ResourceLocation id, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result) {
 | 
			
		||||
        super(id, group, category, width, height, ingredients, result);
 | 
			
		||||
        this.group = group;
 | 
			
		||||
        this.result = result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getGroup() {
 | 
			
		||||
        return group;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ItemStack getResultItem() {
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean matches(CraftingContainer inv, Level world) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public ItemStack assemble(CraftingContainer inventory, RegistryAccess registryAccess) {
 | 
			
		||||
        return ItemStack.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public RecipeSerializer<?> getSerializer() {
 | 
			
		||||
        return ModRegistry.RecipeSerializers.IMPOSTOR_SHAPED.get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static class Serializer implements RecipeSerializer<ImpostorRecipe> {
 | 
			
		||||
        @Override
 | 
			
		||||
        public ImpostorRecipe fromJson(ResourceLocation identifier, JsonObject json) {
 | 
			
		||||
            var group = GsonHelper.getAsString(json, "group", "");
 | 
			
		||||
            var category = CraftingBookCategory.CODEC.byName(GsonHelper.getAsString(json, "category", null), CraftingBookCategory.MISC);
 | 
			
		||||
            var recipe = RecipeSerializer.SHAPED_RECIPE.fromJson(identifier, json);
 | 
			
		||||
            var result = ShapedRecipe.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result"));
 | 
			
		||||
            return new ImpostorRecipe(identifier, group, category, recipe.getWidth(), recipe.getHeight(), recipe.getIngredients(), result);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public ImpostorRecipe fromNetwork(ResourceLocation identifier, FriendlyByteBuf buf) {
 | 
			
		||||
            var width = buf.readVarInt();
 | 
			
		||||
            var height = buf.readVarInt();
 | 
			
		||||
            var group = buf.readUtf(Short.MAX_VALUE);
 | 
			
		||||
            var category = buf.readEnum(CraftingBookCategory.class);
 | 
			
		||||
            var items = NonNullList.withSize(width * height, Ingredient.EMPTY);
 | 
			
		||||
            for (var k = 0; k < items.size(); k++) items.set(k, Ingredient.fromNetwork(buf));
 | 
			
		||||
            var result = buf.readItem();
 | 
			
		||||
            return new ImpostorRecipe(identifier, group, category, width, height, items, result);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void toNetwork(FriendlyByteBuf buf, ImpostorRecipe recipe) {
 | 
			
		||||
            buf.writeVarInt(recipe.getWidth());
 | 
			
		||||
            buf.writeVarInt(recipe.getHeight());
 | 
			
		||||
            buf.writeUtf(recipe.getGroup());
 | 
			
		||||
            buf.writeEnum(recipe.category());
 | 
			
		||||
            for (var ingredient : recipe.getIngredients()) ingredient.toNetwork(buf);
 | 
			
		||||
            buf.writeItem(recipe.getResultItem());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,104 +0,0 @@
 | 
			
		||||
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: LicenseRef-CCPL
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.util;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonArray;
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import com.google.gson.JsonParseException;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import net.minecraft.core.NonNullList;
 | 
			
		||||
import net.minecraft.core.RegistryAccess;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.util.GsonHelper;
 | 
			
		||||
import net.minecraft.world.inventory.CraftingContainer;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.crafting.*;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
 | 
			
		||||
public final class ImpostorShapelessRecipe extends ShapelessRecipe {
 | 
			
		||||
    private final String group;
 | 
			
		||||
    private final ItemStack result;
 | 
			
		||||
 | 
			
		||||
    private ImpostorShapelessRecipe(ResourceLocation id, String group, CraftingBookCategory category, ItemStack result, NonNullList<Ingredient> ingredients) {
 | 
			
		||||
        super(id, group, category, result, ingredients);
 | 
			
		||||
        this.group = group;
 | 
			
		||||
        this.result = result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getGroup() {
 | 
			
		||||
        return group;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ItemStack getResultItem() {
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean matches(CraftingContainer inv, Level world) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public ItemStack assemble(CraftingContainer inventory, RegistryAccess access) {
 | 
			
		||||
        return ItemStack.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public RecipeSerializer<?> getSerializer() {
 | 
			
		||||
        return ModRegistry.RecipeSerializers.IMPOSTOR_SHAPELESS.get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static final class Serializer implements RecipeSerializer<ImpostorShapelessRecipe> {
 | 
			
		||||
        @Override
 | 
			
		||||
        public ImpostorShapelessRecipe fromJson(ResourceLocation id, JsonObject json) {
 | 
			
		||||
            var group = GsonHelper.getAsString(json, "group", "");
 | 
			
		||||
            var category = CraftingBookCategory.CODEC.byName(GsonHelper.getAsString(json, "category", null), CraftingBookCategory.MISC);
 | 
			
		||||
            var ingredients = readIngredients(GsonHelper.getAsJsonArray(json, "ingredients"));
 | 
			
		||||
 | 
			
		||||
            if (ingredients.isEmpty()) throw new JsonParseException("No ingredients for shapeless recipe");
 | 
			
		||||
            if (ingredients.size() > 9) {
 | 
			
		||||
                throw new JsonParseException("Too many ingredients for shapeless recipe the max is 9");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var result = ShapedRecipe.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result"));
 | 
			
		||||
            return new ImpostorShapelessRecipe(id, group, category, result, ingredients);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private NonNullList<Ingredient> readIngredients(JsonArray arrays) {
 | 
			
		||||
            NonNullList<Ingredient> items = NonNullList.create();
 | 
			
		||||
            for (var i = 0; i < arrays.size(); ++i) {
 | 
			
		||||
                var ingredient = Ingredient.fromJson(arrays.get(i));
 | 
			
		||||
                if (!ingredient.isEmpty()) items.add(ingredient);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return items;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public ImpostorShapelessRecipe fromNetwork(ResourceLocation id, FriendlyByteBuf buffer) {
 | 
			
		||||
            var group = buffer.readUtf();
 | 
			
		||||
            var category = buffer.readEnum(CraftingBookCategory.class);
 | 
			
		||||
            var count = buffer.readVarInt();
 | 
			
		||||
            var items = NonNullList.withSize(count, Ingredient.EMPTY);
 | 
			
		||||
 | 
			
		||||
            for (var j = 0; j < items.size(); j++) items.set(j, Ingredient.fromNetwork(buffer));
 | 
			
		||||
            var result = buffer.readItem();
 | 
			
		||||
 | 
			
		||||
            return new ImpostorShapelessRecipe(id, group, category, result, items);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void toNetwork(FriendlyByteBuf buffer, ImpostorShapelessRecipe recipe) {
 | 
			
		||||
            buffer.writeUtf(recipe.getGroup());
 | 
			
		||||
            buffer.writeEnum(recipe.category());
 | 
			
		||||
            buffer.writeVarInt(recipe.getIngredients().size());
 | 
			
		||||
 | 
			
		||||
            for (var ingredient : recipe.getIngredients()) ingredient.toNetwork(buffer);
 | 
			
		||||
            buffer.writeItem(recipe.getResultItem());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,94 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.util;
 | 
			
		||||
 | 
			
		||||
import com.google.common.collect.Maps;
 | 
			
		||||
import com.google.common.collect.Sets;
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import com.google.gson.JsonSyntaxException;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import net.minecraft.core.NonNullList;
 | 
			
		||||
import net.minecraft.util.GsonHelper;
 | 
			
		||||
import net.minecraft.world.item.crafting.Ingredient;
 | 
			
		||||
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
 | 
			
		||||
// TODO: Replace some things with Forge??
 | 
			
		||||
 | 
			
		||||
public final class RecipeUtil {
 | 
			
		||||
    private RecipeUtil() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public record ShapedTemplate(int width, int height, NonNullList<Ingredient> ingredients) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static ShapedTemplate getTemplate(JsonObject json) {
 | 
			
		||||
        Map<Character, Ingredient> ingMap = Maps.newHashMap();
 | 
			
		||||
        for (var entry : GsonHelper.getAsJsonObject(json, "key").entrySet()) {
 | 
			
		||||
            if (entry.getKey().length() != 1) {
 | 
			
		||||
                throw new JsonSyntaxException("Invalid key entry: '" + entry.getKey() + "' is an invalid symbol (must be 1 character only).");
 | 
			
		||||
            }
 | 
			
		||||
            if (" ".equals(entry.getKey())) {
 | 
			
		||||
                throw new JsonSyntaxException("Invalid key entry: ' ' is a reserved symbol.");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            ingMap.put(entry.getKey().charAt(0), Ingredient.fromJson(entry.getValue()));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ingMap.put(' ', Ingredient.EMPTY);
 | 
			
		||||
 | 
			
		||||
        var patternJ = GsonHelper.getAsJsonArray(json, "pattern");
 | 
			
		||||
 | 
			
		||||
        if (patternJ.size() == 0) {
 | 
			
		||||
            throw new JsonSyntaxException("Invalid pattern: empty pattern not allowed");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var pattern = new String[patternJ.size()];
 | 
			
		||||
        for (var x = 0; x < pattern.length; x++) {
 | 
			
		||||
            var line = GsonHelper.convertToString(patternJ.get(x), "pattern[" + x + "]");
 | 
			
		||||
            if (x > 0 && pattern[0].length() != line.length()) {
 | 
			
		||||
                throw new JsonSyntaxException("Invalid pattern: each row must  be the same width");
 | 
			
		||||
            }
 | 
			
		||||
            pattern[x] = line;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var width = pattern[0].length();
 | 
			
		||||
        var height = pattern.length;
 | 
			
		||||
        var ingredients = NonNullList.withSize(width * height, Ingredient.EMPTY);
 | 
			
		||||
 | 
			
		||||
        Set<Character> missingKeys = Sets.newHashSet(ingMap.keySet());
 | 
			
		||||
        missingKeys.remove(' ');
 | 
			
		||||
 | 
			
		||||
        var ingredientIdx = 0;
 | 
			
		||||
        for (var line : pattern) {
 | 
			
		||||
            for (var i = 0; i < line.length(); i++) {
 | 
			
		||||
                var chr = line.charAt(i);
 | 
			
		||||
 | 
			
		||||
                var ing = ingMap.get(chr);
 | 
			
		||||
                if (ing == null) {
 | 
			
		||||
                    throw new JsonSyntaxException("Pattern references symbol '" + chr + "' but it's not defined in the key");
 | 
			
		||||
                }
 | 
			
		||||
                ingredients.set(ingredientIdx++, ing);
 | 
			
		||||
                missingKeys.remove(chr);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!missingKeys.isEmpty()) {
 | 
			
		||||
            throw new JsonSyntaxException("Key defines symbols that aren't used in pattern: " + missingKeys);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new ShapedTemplate(width, height, ingredients);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static ComputerFamily getFamily(JsonObject json, String name) {
 | 
			
		||||
        var familyName = GsonHelper.getAsString(json, name);
 | 
			
		||||
        for (var family : ComputerFamily.values()) {
 | 
			
		||||
            if (family.name().equalsIgnoreCase(familyName)) return family;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new JsonSyntaxException("Unknown computer family '" + familyName + "' for field " + name);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -47,8 +47,6 @@
 | 
			
		||||
    "commands.computercraft.synopsis": "Různé příkazy pro ovládání počítačů.",
 | 
			
		||||
    "commands.computercraft.tp.action": "Teleportovat se k počítači",
 | 
			
		||||
    "commands.computercraft.tp.desc": "Teleportovat se na místo počítače. Můžeš specifikovat ID počítačové instance (tř. 123) nebo ID počítače (tř. #123).",
 | 
			
		||||
    "commands.computercraft.tp.not_player": "Nelze otevřít terminál pro nehráče",
 | 
			
		||||
    "commands.computercraft.tp.not_there": "Nelze najít počítač ve světě",
 | 
			
		||||
    "commands.computercraft.tp.synopsis": "Teleportovat se ke specifickému počítači.",
 | 
			
		||||
    "commands.computercraft.track.desc": "Sledovat jak dlouho se počítače spustí, a také kolik událostí zpracují. Toto uvádí informace v podobné cestě jako /forge track a může být dobré pro diagnostiku lagu.",
 | 
			
		||||
    "commands.computercraft.track.dump.computer": "Počítač",
 | 
			
		||||
 
 | 
			
		||||
@@ -46,8 +46,6 @@
 | 
			
		||||
    "commands.computercraft.synopsis": "Verschiedene Befehle um Computer zu kontrollieren.",
 | 
			
		||||
    "commands.computercraft.tp.action": "Teleportiert dich zum Computer",
 | 
			
		||||
    "commands.computercraft.tp.desc": "Teleportiert dich zum Standort eines Computers. Der Computer kann entweder über seine Instanz ID (z.B. 123), seine Computer ID (z.B. #123) oder seinen Namen (z.B. \"@Mein Computer\") angegeben werden.",
 | 
			
		||||
    "commands.computercraft.tp.not_player": "Konnte Terminal für Nicht-Spieler nicht öffnen",
 | 
			
		||||
    "commands.computercraft.tp.not_there": "Konnte Computer in der Welt nicht finden",
 | 
			
		||||
    "commands.computercraft.tp.synopsis": "Teleportiert dich zum angegebenen Computer.",
 | 
			
		||||
    "commands.computercraft.track.desc": "Zeichnet die Laufzeiten von Computern und wie viele Events ausgelöst werden auf. Die Ausgabe der Informationen ist ähnlich zu /forge track, was beim aufspüren von Lags sehr hilfreich sein kann.",
 | 
			
		||||
    "commands.computercraft.track.dump.computer": "Computer",
 | 
			
		||||
 
 | 
			
		||||
@@ -47,8 +47,6 @@
 | 
			
		||||
    "commands.computercraft.synopsis": "Commandes diverses pour contrôler les ordinateurs.",
 | 
			
		||||
    "commands.computercraft.tp.action": "Se téléporter vers cet ordinateur",
 | 
			
		||||
    "commands.computercraft.tp.desc": "Se téléporter à la position de l'ordinateur. Vous pouvez spécifier l'identifiant d'instance (ex. 123) ou l'identifiant d'ordinateur (ex. #123).",
 | 
			
		||||
    "commands.computercraft.tp.not_player": "Impossible d'ouvrir un terminal pour un non-joueur",
 | 
			
		||||
    "commands.computercraft.tp.not_there": "Impossible de localiser cet ordinateur dans le monde",
 | 
			
		||||
    "commands.computercraft.tp.synopsis": "Se téléporter à la position de l'ordinateur spécifié.",
 | 
			
		||||
    "commands.computercraft.track.desc": "Surveillez combien de temps prend une exécutions sur les ordinateurs, ainsi que le nombre d’événements capturés. Les informations sont affichées d'une manière similaire à la commande /forge track, utile pour diagnostiquer les sources de latence.",
 | 
			
		||||
    "commands.computercraft.track.dump.computer": "Ordinateur",
 | 
			
		||||
 
 | 
			
		||||
@@ -47,8 +47,6 @@
 | 
			
		||||
    "commands.computercraft.synopsis": "Vari comandi per controllare i computer.",
 | 
			
		||||
    "commands.computercraft.tp.action": "Teletrasporta a questo computer",
 | 
			
		||||
    "commands.computercraft.tp.desc": "Teletrasporta alla posizione di un computer. Puoi specificare il computer con l'instance id (e.g. 123) o con l'id (e.g. #123).",
 | 
			
		||||
    "commands.computercraft.tp.not_player": "Non è possibile aprire un terminale per un non giocatore",
 | 
			
		||||
    "commands.computercraft.tp.not_there": "Impossibile trovare il computer nel mondo",
 | 
			
		||||
    "commands.computercraft.tp.synopsis": "Teletrasporta al computer specificato.",
 | 
			
		||||
    "commands.computercraft.track.desc": "Monitora per quanto tempo i computer vengono eseguiti e quanti eventi ricevono. Questo comando fornisce le informazioni in modo simile a /forge track e può essere utile per diagnosticare il lag.",
 | 
			
		||||
    "commands.computercraft.track.dump.computer": "Computer",
 | 
			
		||||
 
 | 
			
		||||
@@ -47,8 +47,6 @@
 | 
			
		||||
    "commands.computercraft.synopsis": "コンピュータを制御するためのさまざまなコマンド。",
 | 
			
		||||
    "commands.computercraft.tp.action": "このコンピューターへテレポートします",
 | 
			
		||||
    "commands.computercraft.tp.desc": "コンピュータの場所にテレポート.コンピュータのインスタンスID(例えば 123)またはコンピュータID(例えば #123)を指定することができます。",
 | 
			
		||||
    "commands.computercraft.tp.not_player": "非プレイヤー用のターミナルを開くことができません",
 | 
			
		||||
    "commands.computercraft.tp.not_there": "世界でコンピュータを見つけることができませんでした",
 | 
			
		||||
    "commands.computercraft.tp.synopsis": "特定のコンピュータにテレポート。",
 | 
			
		||||
    "commands.computercraft.track.desc": "コンピュータの実行時間を追跡するだけでなく、イベントを確認することができます。 これは /forge と同様の方法で情報を提示し、遅れを診断するのに役立ちます。",
 | 
			
		||||
    "commands.computercraft.track.dump.computer": "コンピューター",
 | 
			
		||||
 
 | 
			
		||||
@@ -47,8 +47,6 @@
 | 
			
		||||
    "commands.computercraft.synopsis": "컴퓨터를 제어하기 위한 다양한 명령어",
 | 
			
		||||
    "commands.computercraft.tp.action": "이 컴퓨터로 순간이동하기",
 | 
			
		||||
    "commands.computercraft.tp.desc": "컴퓨터의 위치로 순간이동합니다. 컴퓨터의 인스턴스 ID(예: 123) 또는 컴퓨터 ID(예: #123)를 지정할 수 있습니다.",
 | 
			
		||||
    "commands.computercraft.tp.not_player": "비플레이어용 터미널을 열 수 없습니다.",
 | 
			
		||||
    "commands.computercraft.tp.not_there": "월드에서 컴퓨터를 위치시킬 수 없습니다.",
 | 
			
		||||
    "commands.computercraft.tp.synopsis": "특정 컴퓨터로 순간이동하기",
 | 
			
		||||
    "commands.computercraft.track.desc": "컴퓨터가 실행되는 기간과 처리되는 이벤트 수를 추적합니다. 이는 /forge 트랙과 유사한 방법으로 정보를 제공하며 지연 로그에 유용할 수 있습니다.",
 | 
			
		||||
    "commands.computercraft.track.dump.computer": "컴퓨터",
 | 
			
		||||
 
 | 
			
		||||
@@ -47,8 +47,6 @@
 | 
			
		||||
    "commands.computercraft.synopsis": "Forskjellige kommandoer for å kontrollere datamaskiner.",
 | 
			
		||||
    "commands.computercraft.tp.action": "Teleporter til denne datamaskinen",
 | 
			
		||||
    "commands.computercraft.tp.desc": "Teleporter til en datamaskin sin posisjon. Du kan enten spesifisere datamaskinens forekomst id (f. eks 123) eller datamaskinens id (f. eks #123).",
 | 
			
		||||
    "commands.computercraft.tp.not_player": "Kan kun åpne terminalen for spillere",
 | 
			
		||||
    "commands.computercraft.tp.not_there": "Greide ikke å finne datamaskinen i denne verdenen",
 | 
			
		||||
    "commands.computercraft.tp.synopsis": "Teleporter til en spesifikk datamaskin.",
 | 
			
		||||
    "commands.computercraft.track.desc": "Spor hvor lenge datamaskiner kjører, samt hvor mange hendelser de handler. Dette presenterer informasjon i en lignende vei til /forge track og kan være nyttig for å diagnostisere lagg.",
 | 
			
		||||
    "commands.computercraft.track.dump.computer": "Datamaskin",
 | 
			
		||||
 
 | 
			
		||||
@@ -47,8 +47,6 @@
 | 
			
		||||
    "commands.computercraft.synopsis": "Verschillende commando's voor het beheren van computers.",
 | 
			
		||||
    "commands.computercraft.tp.action": "Teleporteer naar deze computer",
 | 
			
		||||
    "commands.computercraft.tp.desc": "Teleporteer naar de locatie van een specefieke computer. Je kunt een instance-id (bijv. 123) of computer-id (bijv. #123) opgeven.",
 | 
			
		||||
    "commands.computercraft.tp.not_player": "Kan geen terminal openen voor non-speler",
 | 
			
		||||
    "commands.computercraft.tp.not_there": "Kan de computer niet lokaliseren",
 | 
			
		||||
    "commands.computercraft.tp.synopsis": "Teleporteer naar een specifieke computer.",
 | 
			
		||||
    "commands.computercraft.track.desc": "Houd uitvoertijd en het aantal behandelde events van computers bij. Dit biedt informatie op een gelijke manier als /forge track en kan nuttig zijn in het opsloren van lag.",
 | 
			
		||||
    "commands.computercraft.track.dump.computer": "Computer",
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,6 @@
 | 
			
		||||
    "commands.computercraft.synopsis": "Różne komendy do kontrolowania komputerami.",
 | 
			
		||||
    "commands.computercraft.tp.action": "Przeteleportuj się do podanego komputera",
 | 
			
		||||
    "commands.computercraft.tp.desc": "Przeteleportuj się do lokalizacji komputera. Możesz wybrać numer sesji komputera (np. 123) lub ID komputera (np. #123).",
 | 
			
		||||
    "commands.computercraft.tp.not_there": "Nie można zlokalizować komputera w świecie",
 | 
			
		||||
    "commands.computercraft.tp.synopsis": "Przeteleportuj się do podanego komputera.",
 | 
			
		||||
    "commands.computercraft.track.dump.computer": "Komputer",
 | 
			
		||||
    "commands.computercraft.turn_on.desc": "Włącz podane komputery. Możesz wybrać numer sesji komputera (np. 123), ID komputera (np. #123) lub jego etykietę (np. \"@Mój Komputer\").",
 | 
			
		||||
 
 | 
			
		||||
@@ -47,8 +47,6 @@
 | 
			
		||||
    "commands.computercraft.synopsis": "Различные команды для управления компьютерами.",
 | 
			
		||||
    "commands.computercraft.tp.action": "Телепортироваться к этому компьютеру",
 | 
			
		||||
    "commands.computercraft.tp.desc": "Телепортироваться к местоположению компьютера. Ты можешь указать либо идентификатор экземпляра компьютера (например 123) либо идентификатор компьютера (например #123).",
 | 
			
		||||
    "commands.computercraft.tp.not_player": "Нельзя открыть терминал для не-игрока",
 | 
			
		||||
    "commands.computercraft.tp.not_there": "Нельзя определить в мире местоположение компьютер",
 | 
			
		||||
    "commands.computercraft.tp.synopsis": "Телепортироваться к конкретному компьютеру.",
 | 
			
		||||
    "commands.computercraft.track.desc": "Отслеживает, как долго компьютеры исполняют, а также то, как много они обрабатывают события. Эта информация представляется аналогично к /forge track и может быть полезной для диагностики лага.",
 | 
			
		||||
    "commands.computercraft.track.dump.computer": "Компьютер",
 | 
			
		||||
 
 | 
			
		||||
@@ -47,8 +47,6 @@
 | 
			
		||||
    "commands.computercraft.synopsis": "Olika kommandon för att kontrollera datorer.",
 | 
			
		||||
    "commands.computercraft.tp.action": "Teleportera till den här datorn",
 | 
			
		||||
    "commands.computercraft.tp.desc": "Teleportera till datorns position. Du kan ange en dators instans-id (t.ex. 123), dator-id (t.ex. #123) eller etikett (t.ex. \"@Min dator\").",
 | 
			
		||||
    "commands.computercraft.tp.not_player": "Kan inte öppna terminalen för en ickespelare",
 | 
			
		||||
    "commands.computercraft.tp.not_there": "Kan inte hitta datorn i världen",
 | 
			
		||||
    "commands.computercraft.tp.synopsis": "Teleportera till en specifik dator.",
 | 
			
		||||
    "commands.computercraft.track.desc": "Spåra hur länge datorer exekverar, och även hur många event de hanterar. Detta presenterar information på liknande sätt som /forge track och kan vara användbart för att undersöka lagg.",
 | 
			
		||||
    "commands.computercraft.track.dump.computer": "Dator",
 | 
			
		||||
 
 | 
			
		||||
@@ -44,8 +44,6 @@
 | 
			
		||||
    "commands.computercraft.synopsis": "mi jo e toki wawa ante tawa ni: sina lawa e ilo sona.",
 | 
			
		||||
    "commands.computercraft.tp.action": "o tawa pi ilo sona",
 | 
			
		||||
    "commands.computercraft.tp.desc": "o tawa ilo sona. ilo sona la, sina ken pana e nanpa pi ijo (sama 123) anu nanpa (sama #123).",
 | 
			
		||||
    "commands.computercraft.tp.not_player": "jan ala la, mi ken ala open e sitelen pi ilo sona",
 | 
			
		||||
    "commands.computercraft.tp.not_there": "mi ken ala alasa e ilo sona pi lon ma",
 | 
			
		||||
    "commands.computercraft.tp.synopsis": "mi tawa pi ilo sona e sina.",
 | 
			
		||||
    "commands.computercraft.track.desc": "ilo sona la, mi sitelen e tenpo pali e mute toki. toki wawa /forge track la, mi pana pi nasin sama e sona. mi pona tawa alasa pi tenpo ike.",
 | 
			
		||||
    "commands.computercraft.track.dump.computer": "ilo sona",
 | 
			
		||||
 
 | 
			
		||||
@@ -47,8 +47,6 @@
 | 
			
		||||
    "commands.computercraft.synopsis": "Різні команди для керування комп'ютерами.",
 | 
			
		||||
    "commands.computercraft.tp.action": "Телепортувати до цього комп'ютера",
 | 
			
		||||
    "commands.computercraft.tp.desc": "Телепортувати до розташування комп'ютера. Ви можете вказати або ідентифікатор екземпляра комп'ютера (наприклад, 123) або ідентифікатор комп'ютера (наприклад, #123).",
 | 
			
		||||
    "commands.computercraft.tp.not_player": "Не можна відкрити термінал для не-гравця",
 | 
			
		||||
    "commands.computercraft.tp.not_there": "Не можна визначити у світі розташування комп'ютер",
 | 
			
		||||
    "commands.computercraft.tp.synopsis": "Телепортувати до конкретного комп'ютера.",
 | 
			
		||||
    "commands.computercraft.track.desc": "Відстежує, як довго комп'ютери виконують, а також те, як багато вони обробляють події. Ця інформація представляється аналогічно /forge track і може бути корисною для діагностики лага.",
 | 
			
		||||
    "commands.computercraft.track.dump.computer": "Комп'ютер",
 | 
			
		||||
 
 | 
			
		||||
@@ -45,8 +45,6 @@
 | 
			
		||||
    "commands.computercraft.synopsis": "各种控制计算机的命令.",
 | 
			
		||||
    "commands.computercraft.tp.action": "传送到这台电脑",
 | 
			
		||||
    "commands.computercraft.tp.desc": "传送到计算机的位置. 你可以指定计算机的实例id (例如. 123)或计算机id (例如. #123).",
 | 
			
		||||
    "commands.computercraft.tp.not_player": "无法为非玩家打开终端",
 | 
			
		||||
    "commands.computercraft.tp.not_there": "无法在世界上定位电脑",
 | 
			
		||||
    "commands.computercraft.tp.synopsis": "传送到特定的计算机.",
 | 
			
		||||
    "commands.computercraft.track.desc": "跟踪计算机执行的时间以及它们处理的事件数. 这以/forge track类似的方式呈现信息,可用于诊断滞后.",
 | 
			
		||||
    "commands.computercraft.track.dump.computer": "计算机",
 | 
			
		||||
 
 | 
			
		||||
@@ -353,7 +353,7 @@ public class NetworkTest {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static class NetworkPeripheral implements IPeripheral {
 | 
			
		||||
    private static final class NetworkPeripheral implements IPeripheral {
 | 
			
		||||
        @Override
 | 
			
		||||
        public String getType() {
 | 
			
		||||
            return "test";
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,50 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.recipe;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.test.shared.MinecraftArbitraries;
 | 
			
		||||
import it.unimi.dsi.fastutil.ints.IntIntImmutablePair;
 | 
			
		||||
import net.jqwik.api.Arbitraries;
 | 
			
		||||
import net.jqwik.api.Arbitrary;
 | 
			
		||||
import net.jqwik.api.Combinators;
 | 
			
		||||
import net.minecraft.core.NonNullList;
 | 
			
		||||
import net.minecraft.world.item.crafting.CraftingBookCategory;
 | 
			
		||||
import net.minecraft.world.item.crafting.Ingredient;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * {@link Arbitrary} implementations for recipes.
 | 
			
		||||
 */
 | 
			
		||||
public final class RecipeArbitraries {
 | 
			
		||||
    public static Arbitrary<RecipeProperties> recipeProperties() {
 | 
			
		||||
        return Combinators.combine(
 | 
			
		||||
            Arbitraries.strings().ofMinLength(1).withChars("abcdefghijklmnopqrstuvwxyz_"),
 | 
			
		||||
            Arbitraries.of(CraftingBookCategory.values())
 | 
			
		||||
        ).as(RecipeProperties::new);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static Arbitrary<ShapelessRecipeSpec> shapelessRecipeSpec() {
 | 
			
		||||
        return Combinators.combine(
 | 
			
		||||
            recipeProperties(),
 | 
			
		||||
            MinecraftArbitraries.ingredient().array(Ingredient[].class).ofMinSize(1).map(x -> NonNullList.of(Ingredient.EMPTY, x)),
 | 
			
		||||
            MinecraftArbitraries.nonEmptyItemStack()
 | 
			
		||||
        ).as(ShapelessRecipeSpec::new);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static Arbitrary<ShapedTemplate> shapedTemplate() {
 | 
			
		||||
        return Combinators.combine(Arbitraries.integers().between(1, 3), Arbitraries.integers().between(1, 3))
 | 
			
		||||
            .as(IntIntImmutablePair::new)
 | 
			
		||||
            .flatMap(x -> MinecraftArbitraries.ingredient().array(Ingredient[].class).ofSize(x.leftInt() * x.rightInt())
 | 
			
		||||
                .map(i -> new ShapedTemplate(x.leftInt(), x.rightInt(), NonNullList.of(Ingredient.EMPTY, i)))
 | 
			
		||||
            );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static Arbitrary<ShapedRecipeSpec> shapedRecipeSpec() {
 | 
			
		||||
        return Combinators.combine(
 | 
			
		||||
            recipeProperties(),
 | 
			
		||||
            shapedTemplate(),
 | 
			
		||||
            MinecraftArbitraries.nonEmptyItemStack()
 | 
			
		||||
        ).as(ShapedRecipeSpec::new);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,34 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.recipe;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.test.core.StructuralEquality;
 | 
			
		||||
import dan200.computercraft.test.shared.MinecraftEqualities;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * {@link StructuralEquality} implementations for recipes.
 | 
			
		||||
 */
 | 
			
		||||
public final class RecipeEqualities {
 | 
			
		||||
    private RecipeEqualities() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static final StructuralEquality<ShapelessRecipeSpec> shapelessRecipeSpec = StructuralEquality.all(
 | 
			
		||||
        StructuralEquality.at("properties", ShapelessRecipeSpec::properties),
 | 
			
		||||
        StructuralEquality.at("ingredients", ShapelessRecipeSpec::ingredients, MinecraftEqualities.ingredient.list()),
 | 
			
		||||
        StructuralEquality.at("result", ShapelessRecipeSpec::result, MinecraftEqualities.itemStack)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    public static final StructuralEquality<ShapedTemplate> shapedTemplate = StructuralEquality.all(
 | 
			
		||||
        StructuralEquality.at("width", ShapedTemplate::width),
 | 
			
		||||
        StructuralEquality.at("height", ShapedTemplate::height),
 | 
			
		||||
        StructuralEquality.at("ingredients", ShapedTemplate::ingredients, MinecraftEqualities.ingredient.list())
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    public static final StructuralEquality<ShapedRecipeSpec> shapedRecipeSpec = StructuralEquality.all(
 | 
			
		||||
        StructuralEquality.at("properties", ShapedRecipeSpec::properties),
 | 
			
		||||
        StructuralEquality.at("ingredients", ShapedRecipeSpec::template, shapedTemplate),
 | 
			
		||||
        StructuralEquality.at("result", ShapedRecipeSpec::result, MinecraftEqualities.itemStack)
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,32 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.recipe;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.test.shared.NetworkSupport;
 | 
			
		||||
import dan200.computercraft.test.shared.WithMinecraft;
 | 
			
		||||
import net.jqwik.api.Arbitrary;
 | 
			
		||||
import net.jqwik.api.ForAll;
 | 
			
		||||
import net.jqwik.api.Property;
 | 
			
		||||
import net.jqwik.api.Provide;
 | 
			
		||||
 | 
			
		||||
import static org.hamcrest.MatcherAssert.assertThat;
 | 
			
		||||
 | 
			
		||||
@WithMinecraft
 | 
			
		||||
public class ShapedRecipeSpecTest {
 | 
			
		||||
    static {
 | 
			
		||||
        WithMinecraft.Setup.bootstrap(); // @Property doesn't run test lifecycle methods.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Property
 | 
			
		||||
    public void testRoundTrip(@ForAll("recipe") ShapedRecipeSpec spec) {
 | 
			
		||||
        var converted = NetworkSupport.roundTrip(spec, ShapedRecipeSpec::toNetwork, ShapedRecipeSpec::fromNetwork);
 | 
			
		||||
        assertThat("Recipes are equal", converted, RecipeEqualities.shapedRecipeSpec.asMatcher(ShapedRecipeSpec.class, spec));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Provide
 | 
			
		||||
    Arbitrary<ShapedRecipeSpec> recipe() {
 | 
			
		||||
        return RecipeArbitraries.shapedRecipeSpec();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,32 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.recipe;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.test.shared.NetworkSupport;
 | 
			
		||||
import dan200.computercraft.test.shared.WithMinecraft;
 | 
			
		||||
import net.jqwik.api.Arbitrary;
 | 
			
		||||
import net.jqwik.api.ForAll;
 | 
			
		||||
import net.jqwik.api.Property;
 | 
			
		||||
import net.jqwik.api.Provide;
 | 
			
		||||
 | 
			
		||||
import static org.hamcrest.MatcherAssert.assertThat;
 | 
			
		||||
 | 
			
		||||
@WithMinecraft
 | 
			
		||||
public class ShapelessRecipeSpecTest {
 | 
			
		||||
    static {
 | 
			
		||||
        WithMinecraft.Setup.bootstrap(); // @Property doesn't run test lifecycle methods.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Property
 | 
			
		||||
    public void testRoundTrip(@ForAll("recipe") ShapelessRecipeSpec spec) {
 | 
			
		||||
        var converted = NetworkSupport.roundTrip(spec, ShapelessRecipeSpec::toNetwork, ShapelessRecipeSpec::fromNetwork);
 | 
			
		||||
        assertThat("Recipes are equal", converted, RecipeEqualities.shapelessRecipeSpec.asMatcher(ShapelessRecipeSpec.class, spec));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Provide
 | 
			
		||||
    Arbitrary<ShapelessRecipeSpec> recipe() {
 | 
			
		||||
        return RecipeArbitraries.shapelessRecipeSpec();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -8,16 +8,13 @@ import dan200.computercraft.api.turtle.ITurtleUpgrade;
 | 
			
		||||
import dan200.computercraft.api.turtle.TurtleToolDurability;
 | 
			
		||||
import dan200.computercraft.test.core.StructuralEquality;
 | 
			
		||||
import dan200.computercraft.test.shared.MinecraftArbitraries;
 | 
			
		||||
import dan200.computercraft.test.shared.MinecraftEqualities;
 | 
			
		||||
import dan200.computercraft.test.shared.NetworkSupport;
 | 
			
		||||
import dan200.computercraft.test.shared.WithMinecraft;
 | 
			
		||||
import io.netty.buffer.Unpooled;
 | 
			
		||||
import net.jqwik.api.*;
 | 
			
		||||
import net.minecraft.core.registries.Registries;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import org.hamcrest.Description;
 | 
			
		||||
 | 
			
		||||
import static org.hamcrest.MatcherAssert.assertThat;
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertEquals;
 | 
			
		||||
 | 
			
		||||
@WithMinecraft
 | 
			
		||||
class TurtleToolSerialiserTest {
 | 
			
		||||
@@ -32,11 +29,9 @@ class TurtleToolSerialiserTest {
 | 
			
		||||
     */
 | 
			
		||||
    @Property
 | 
			
		||||
    public void testRoundTrip(@ForAll("tool") TurtleTool tool) {
 | 
			
		||||
        var buffer = new FriendlyByteBuf(Unpooled.directBuffer());
 | 
			
		||||
        TurtleToolSerialiser.INSTANCE.toNetwork(buffer, tool);
 | 
			
		||||
 | 
			
		||||
        var converted = TurtleToolSerialiser.INSTANCE.fromNetwork(tool.getUpgradeID(), buffer);
 | 
			
		||||
        assertEquals(buffer.readableBytes(), 0, "Whole packet was read");
 | 
			
		||||
        var converted = NetworkSupport.roundTripSerialiser(
 | 
			
		||||
            tool.getUpgradeID(), tool, TurtleToolSerialiser.INSTANCE::toNetwork, TurtleToolSerialiser.INSTANCE::fromNetwork
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (!equality.equals(tool, converted)) {
 | 
			
		||||
            System.out.println("Break");
 | 
			
		||||
@@ -58,22 +53,10 @@ class TurtleToolSerialiserTest {
 | 
			
		||||
        ).as(TurtleTool::new);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static final StructuralEquality<ItemStack> stackEquality = new StructuralEquality<>() {
 | 
			
		||||
        @Override
 | 
			
		||||
        public boolean equals(ItemStack left, ItemStack right) {
 | 
			
		||||
            return ItemStack.isSameItemSameTags(left, right) && left.getCount() == right.getCount();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void describe(Description description, ItemStack object) {
 | 
			
		||||
            description.appendValue(object).appendValue(object.getTag());
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private static final StructuralEquality<TurtleTool> equality = StructuralEquality.all(
 | 
			
		||||
        StructuralEquality.at("id", ITurtleUpgrade::getUpgradeID),
 | 
			
		||||
        StructuralEquality.at("craftingItem", ITurtleUpgrade::getCraftingItem, stackEquality),
 | 
			
		||||
        StructuralEquality.at("tool", x -> x.item, stackEquality),
 | 
			
		||||
        StructuralEquality.at("craftingItem", ITurtleUpgrade::getCraftingItem, MinecraftEqualities.itemStack),
 | 
			
		||||
        StructuralEquality.at("tool", x -> x.item, MinecraftEqualities.itemStack),
 | 
			
		||||
        StructuralEquality.at("damageMulitiplier", x -> x.damageMulitiplier),
 | 
			
		||||
        StructuralEquality.at("allowEnchantments", x -> x.allowEnchantments),
 | 
			
		||||
        StructuralEquality.at("consumeDurability", x -> x.consumeDurability),
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ import net.minecraft.tags.TagKey;
 | 
			
		||||
import net.minecraft.world.item.Item;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.Items;
 | 
			
		||||
import net.minecraft.world.item.crafting.Ingredient;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
@@ -44,6 +45,10 @@ public final class MinecraftArbitraries {
 | 
			
		||||
        return Arbitraries.oneOf(List.of(Arbitraries.just(ItemStack.EMPTY), nonEmptyItemStack()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static Arbitrary<Ingredient> ingredient() {
 | 
			
		||||
        return nonEmptyItemStack().list().ofMinSize(1).map(x -> Ingredient.of(x.stream()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static Arbitrary<BlockPos> blockPos() {
 | 
			
		||||
        // BlockPos has a maximum range that can be sent over the network - use those.
 | 
			
		||||
        var xz = Arbitraries.integers().between(-3_000_000, -3_000_000);
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,39 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.test.shared;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.test.core.StructuralEquality;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.crafting.Ingredient;
 | 
			
		||||
import org.hamcrest.Description;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * {@link StructuralEquality} implementations for Minecraft types.
 | 
			
		||||
 */
 | 
			
		||||
public class MinecraftEqualities {
 | 
			
		||||
    public static final StructuralEquality<ItemStack> itemStack = new StructuralEquality<>() {
 | 
			
		||||
        @Override
 | 
			
		||||
        public boolean equals(ItemStack left, ItemStack right) {
 | 
			
		||||
            return ItemStack.isSameItemSameTags(left, right) && left.getCount() == right.getCount();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void describe(Description description, ItemStack object) {
 | 
			
		||||
            description.appendValue(object).appendValue(object.getTag());
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    public static final StructuralEquality<Ingredient> ingredient = new StructuralEquality<>() {
 | 
			
		||||
        @Override
 | 
			
		||||
        public boolean equals(Ingredient left, Ingredient right) {
 | 
			
		||||
            return left.toJson().equals(right.toJson());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void describe(Description description, Ingredient object) {
 | 
			
		||||
            description.appendValue(object.toJson());
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,56 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.test.shared;
 | 
			
		||||
 | 
			
		||||
import io.netty.buffer.Unpooled;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.item.crafting.RecipeSerializer;
 | 
			
		||||
 | 
			
		||||
import java.util.function.BiConsumer;
 | 
			
		||||
import java.util.function.BiFunction;
 | 
			
		||||
import java.util.function.Function;
 | 
			
		||||
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertEquals;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Support methods for working with Minecraft's networking code.
 | 
			
		||||
 */
 | 
			
		||||
public final class NetworkSupport {
 | 
			
		||||
    private NetworkSupport() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Attempt to serialise and then deserialise a value.
 | 
			
		||||
     *
 | 
			
		||||
     * @param value The value to serialise.
 | 
			
		||||
     * @param write Serialise this value to a buffer.
 | 
			
		||||
     * @param read  Deserialise this value from a buffer.
 | 
			
		||||
     * @param <T>   The type of the value to round trip.
 | 
			
		||||
     * @return The converted value, for checking equivalency.
 | 
			
		||||
     */
 | 
			
		||||
    public static <T> T roundTrip(T value, BiConsumer<T, FriendlyByteBuf> write, Function<FriendlyByteBuf, T> read) {
 | 
			
		||||
        var buffer = new FriendlyByteBuf(Unpooled.directBuffer());
 | 
			
		||||
        write.accept(value, buffer);
 | 
			
		||||
 | 
			
		||||
        var converted = read.apply(buffer);
 | 
			
		||||
        assertEquals(buffer.readableBytes(), 0, "Whole packet was read");
 | 
			
		||||
        return converted;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Attempt to serialise and then deserialise a value from a {@link RecipeSerializer}-like interface.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id    The id of this value.
 | 
			
		||||
     * @param value The value to serialise.
 | 
			
		||||
     * @param write Serialise this value to a buffer.
 | 
			
		||||
     * @param read  Deserialise this value from a buffer.
 | 
			
		||||
     * @param <T>   The type of the value to round trip.
 | 
			
		||||
     * @return The converted value, for checking equivalency.
 | 
			
		||||
     */
 | 
			
		||||
    public static <T> T roundTripSerialiser(ResourceLocation id, T value, BiConsumer<FriendlyByteBuf, T> write, BiFunction<ResourceLocation, FriendlyByteBuf, T> read) {
 | 
			
		||||
        return roundTrip(value, (x, b) -> write.accept(b, x), b -> read.apply(id, b));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.gametest
 | 
			
		||||
 | 
			
		||||
import com.mojang.authlib.GameProfile
 | 
			
		||||
import dan200.computercraft.gametest.api.Structures
 | 
			
		||||
import dan200.computercraft.gametest.api.sequence
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry
 | 
			
		||||
@@ -11,6 +12,7 @@ import net.minecraft.gametest.framework.GameTest
 | 
			
		||||
import net.minecraft.gametest.framework.GameTestAssertException
 | 
			
		||||
import net.minecraft.gametest.framework.GameTestHelper
 | 
			
		||||
import net.minecraft.nbt.CompoundTag
 | 
			
		||||
import net.minecraft.nbt.NbtUtils
 | 
			
		||||
import net.minecraft.world.entity.player.Player
 | 
			
		||||
import net.minecraft.world.inventory.AbstractContainerMenu
 | 
			
		||||
import net.minecraft.world.inventory.MenuType
 | 
			
		||||
@@ -41,11 +43,10 @@ class Recipe_Test {
 | 
			
		||||
 | 
			
		||||
            val result = recipe.get().assemble(container, context.level.registryAccess())
 | 
			
		||||
 | 
			
		||||
            val owner = CompoundTag()
 | 
			
		||||
            owner.putString("Name", "dan200")
 | 
			
		||||
            owner.putString("Id", "f3c8d69b-0776-4512-8434-d1b2165909eb")
 | 
			
		||||
            val profile = GameProfile(UUID.fromString("f3c8d69b-0776-4512-8434-d1b2165909eb"), "dan200")
 | 
			
		||||
 | 
			
		||||
            val tag = CompoundTag()
 | 
			
		||||
            tag.put("SkullOwner", owner)
 | 
			
		||||
            tag.put("SkullOwner", NbtUtils.writeGameProfile(CompoundTag(), profile))
 | 
			
		||||
 | 
			
		||||
            assertEquals(tag, result.tag, "Expected NBT tags to be the same")
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -18,9 +18,7 @@ val docApi by configurations.registering {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
dependencies {
 | 
			
		||||
    compileOnlyApi(libs.jsr305)
 | 
			
		||||
    compileOnlyApi(libs.checkerFramework)
 | 
			
		||||
    compileOnlyApi(libs.jetbrainsAnnotations)
 | 
			
		||||
    compileOnlyApi(libs.bundles.annotations)
 | 
			
		||||
 | 
			
		||||
    "docApi"(project(":common-api"))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,25 +12,30 @@ import java.time.Instant;
 | 
			
		||||
/**
 | 
			
		||||
 * A simple version of {@link BasicFileAttributes}, which provides what information a {@link Mount} already exposes.
 | 
			
		||||
 *
 | 
			
		||||
 * @param isDirectory Whether this filesystem entry is a directory.
 | 
			
		||||
 * @param size        The size of the file.
 | 
			
		||||
 * @param isDirectory      Whether this filesystem entry is a directory.
 | 
			
		||||
 * @param size             The size of the file.
 | 
			
		||||
 * @param creationTime     The time the file was created.
 | 
			
		||||
 * @param lastModifiedTime The time the file was last modified.
 | 
			
		||||
 */
 | 
			
		||||
public record FileAttributes(boolean isDirectory, long size) implements BasicFileAttributes {
 | 
			
		||||
public record FileAttributes(
 | 
			
		||||
    boolean isDirectory, long size, FileTime creationTime, FileTime lastModifiedTime
 | 
			
		||||
) implements BasicFileAttributes {
 | 
			
		||||
    private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public FileTime lastModifiedTime() {
 | 
			
		||||
        return EPOCH;
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a new {@link FileAttributes} instance with the {@linkplain #creationTime() creation time} and
 | 
			
		||||
     * {@linkplain #lastModifiedTime() last modified time} set to the Unix epoch.
 | 
			
		||||
     *
 | 
			
		||||
     * @param isDirectory Whether the filesystem entry is a directory.
 | 
			
		||||
     * @param size        The size of the file.
 | 
			
		||||
     */
 | 
			
		||||
    public FileAttributes(boolean isDirectory, long size) {
 | 
			
		||||
        this(isDirectory, size, EPOCH, EPOCH);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public FileTime lastAccessTime() {
 | 
			
		||||
        return EPOCH;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public FileTime creationTime() {
 | 
			
		||||
        return EPOCH;
 | 
			
		||||
        return lastModifiedTime();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,6 @@ import dan200.computercraft.core.filesystem.FileSystemException;
 | 
			
		||||
import dan200.computercraft.core.metrics.Metrics;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.nio.file.attribute.FileTime;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.function.Function;
 | 
			
		||||
@@ -488,9 +487,9 @@ public class FSAPI implements ILuaAPI {
 | 
			
		||||
        try (var ignored = environment.time(Metrics.FS_OPS)) {
 | 
			
		||||
            var attributes = getFileSystem().getAttributes(path);
 | 
			
		||||
            Map<String, Object> result = new HashMap<>();
 | 
			
		||||
            result.put("modification", getFileTime(attributes.lastModifiedTime()));
 | 
			
		||||
            result.put("modified", getFileTime(attributes.lastModifiedTime()));
 | 
			
		||||
            result.put("created", getFileTime(attributes.creationTime()));
 | 
			
		||||
            result.put("modification", attributes.lastModifiedTime().toMillis());
 | 
			
		||||
            result.put("modified", attributes.lastModifiedTime().toMillis());
 | 
			
		||||
            result.put("created", attributes.creationTime().toMillis());
 | 
			
		||||
            result.put("size", attributes.isDirectory() ? 0 : attributes.size());
 | 
			
		||||
            result.put("isDir", attributes.isDirectory());
 | 
			
		||||
            result.put("isReadOnly", getFileSystem().isReadOnly(path));
 | 
			
		||||
@@ -499,8 +498,4 @@ public class FSAPI implements ILuaAPI {
 | 
			
		||||
            throw new LuaException(e.getMessage());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static long getFileTime(@Nullable FileTime time) {
 | 
			
		||||
        return time == null ? 0 : time.toMillis();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ import dan200.computercraft.core.CoreConfig;
 | 
			
		||||
import dan200.computercraft.core.apis.http.*;
 | 
			
		||||
import dan200.computercraft.core.apis.http.request.HttpRequest;
 | 
			
		||||
import dan200.computercraft.core.apis.http.websocket.Websocket;
 | 
			
		||||
import dan200.computercraft.core.apis.http.websocket.WebsocketClient;
 | 
			
		||||
import io.netty.handler.codec.http.DefaultHttpHeaders;
 | 
			
		||||
import io.netty.handler.codec.http.HttpHeaderNames;
 | 
			
		||||
import io.netty.handler.codec.http.HttpHeaders;
 | 
			
		||||
@@ -165,7 +166,7 @@ public class HTTPAPI implements ILuaAPI {
 | 
			
		||||
        var timeout = getTimeout(timeoutArg);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            var uri = Websocket.checkUri(address);
 | 
			
		||||
            var uri = WebsocketClient.parseUri(address);
 | 
			
		||||
            if (!new Websocket(websockets, apiEnvironment, uri, address, headers, timeout).queue(Websocket::connect)) {
 | 
			
		||||
                throw new LuaException("Too many websockets already open");
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,7 @@ import java.util.List;
 | 
			
		||||
 * end
 | 
			
		||||
 * }</pre>
 | 
			
		||||
 * <p>
 | 
			
		||||
 * [comparator]: https://minecraft.gamepedia.com/Redstone_Comparator#Subtract_signal_strength "Redstone Comparator on
 | 
			
		||||
 * [comparator]: https://minecraft.wiki/w/Redstone_Comparator#Subtract_signal_strength "Redstone Comparator on
 | 
			
		||||
 * the Minecraft wiki."
 | 
			
		||||
 * @cc.module redstone
 | 
			
		||||
 */
 | 
			
		||||
 
 | 
			
		||||
@@ -77,12 +77,10 @@ public class BinaryReadableHandle extends HandleGeneric {
 | 
			
		||||
                    var buffer = ByteBuffer.allocate(BUFFER_SIZE);
 | 
			
		||||
                    var read = channel.read(buffer);
 | 
			
		||||
                    if (read < 0) return null;
 | 
			
		||||
                    buffer.flip();
 | 
			
		||||
 | 
			
		||||
                    // If we failed to read "enough" here, let's just abort
 | 
			
		||||
                    if (read >= count || read < BUFFER_SIZE) {
 | 
			
		||||
                        buffer.flip();
 | 
			
		||||
                        return new Object[]{ buffer };
 | 
			
		||||
                    }
 | 
			
		||||
                    if (read >= count || read < BUFFER_SIZE) return new Object[]{ buffer };
 | 
			
		||||
 | 
			
		||||
                    // Build up an array of ByteBuffers. Hopefully this means we can perform less allocation
 | 
			
		||||
                    // than doubling up the buffer each time.
 | 
			
		||||
@@ -90,11 +88,13 @@ public class BinaryReadableHandle extends HandleGeneric {
 | 
			
		||||
                    List<ByteBuffer> parts = new ArrayList<>(4);
 | 
			
		||||
                    parts.add(buffer);
 | 
			
		||||
                    while (read >= BUFFER_SIZE && totalRead < count) {
 | 
			
		||||
                        buffer = ByteBuffer.allocate(Math.min(BUFFER_SIZE, count - totalRead));
 | 
			
		||||
                        buffer = ByteBuffer.allocateDirect(Math.min(BUFFER_SIZE, count - totalRead));
 | 
			
		||||
                        read = channel.read(buffer);
 | 
			
		||||
                        if (read < 0) break;
 | 
			
		||||
                        buffer.flip();
 | 
			
		||||
 | 
			
		||||
                        totalRead += read;
 | 
			
		||||
                        assert read == buffer.remaining();
 | 
			
		||||
                        parts.add(buffer);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
@@ -102,9 +102,11 @@ public class BinaryReadableHandle extends HandleGeneric {
 | 
			
		||||
                    var bytes = new byte[totalRead];
 | 
			
		||||
                    var pos = 0;
 | 
			
		||||
                    for (var part : parts) {
 | 
			
		||||
                        System.arraycopy(part.array(), 0, bytes, pos, part.position());
 | 
			
		||||
                        pos += part.position();
 | 
			
		||||
                        int length = part.remaining();
 | 
			
		||||
                        part.get(bytes, pos, length);
 | 
			
		||||
                        pos += length;
 | 
			
		||||
                    }
 | 
			
		||||
                    assert pos == totalRead;
 | 
			
		||||
                    return new Object[]{ bytes };
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -134,7 +134,7 @@ public final class NetworkUtils {
 | 
			
		||||
     */
 | 
			
		||||
    public static Options getOptions(String host, InetSocketAddress address) throws HTTPRequestException {
 | 
			
		||||
        var options = AddressRule.apply(CoreConfig.httpRules, host, address);
 | 
			
		||||
        if (options.action == Action.DENY) throw new HTTPRequestException("Domain not permitted");
 | 
			
		||||
        if (options.action() == Action.DENY) throw new HTTPRequestException("Domain not permitted");
 | 
			
		||||
        return options;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -150,7 +150,7 @@ public final class NetworkUtils {
 | 
			
		||||
     * @throws HTTPRequestException If a proxy is required but not configured correctly.
 | 
			
		||||
     */
 | 
			
		||||
    public static @Nullable Consumer<SocketChannel> getProxyHandler(Options options, int timeout) throws HTTPRequestException {
 | 
			
		||||
        if (!options.useProxy) return null;
 | 
			
		||||
        if (!options.useProxy()) return null;
 | 
			
		||||
 | 
			
		||||
        var type = CoreConfig.httpProxyType;
 | 
			
		||||
        var host = CoreConfig.httpProxyHost;
 | 
			
		||||
 
 | 
			
		||||
@@ -6,20 +6,13 @@ package dan200.computercraft.core.apis.http.options;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Options about a specific domain.
 | 
			
		||||
 * Options for a given HTTP request or websocket, which control its resource constraints.
 | 
			
		||||
 *
 | 
			
		||||
 * @param action           Whether to {@link Action#ALLOW} or {@link Action#DENY} this request.
 | 
			
		||||
 * @param maxUpload        The maximum size of the HTTP request.
 | 
			
		||||
 * @param maxDownload      The maximum size of the HTTP response.
 | 
			
		||||
 * @param websocketMessage The maximum size of a websocket message (outgoing and incoming).
 | 
			
		||||
 * @param useProxy         Whether to use the configured proxy.
 | 
			
		||||
 */
 | 
			
		||||
public final class Options {
 | 
			
		||||
    public final Action action;
 | 
			
		||||
    public final long maxUpload;
 | 
			
		||||
    public final long maxDownload;
 | 
			
		||||
    public final int websocketMessage;
 | 
			
		||||
    public final boolean useProxy;
 | 
			
		||||
 | 
			
		||||
    Options(Action action, long maxUpload, long maxDownload, int websocketMessage, boolean useProxy) {
 | 
			
		||||
        this.action = action;
 | 
			
		||||
        this.maxUpload = maxUpload;
 | 
			
		||||
        this.maxDownload = maxDownload;
 | 
			
		||||
        this.websocketMessage = websocketMessage;
 | 
			
		||||
        this.useProxy = useProxy;
 | 
			
		||||
    }
 | 
			
		||||
public record Options(Action action, long maxUpload, long maxDownload, int websocketMessage, boolean useProxy) {
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@ public final class PartialOptions {
 | 
			
		||||
        this.useProxy = useProxy;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Options toOptions() {
 | 
			
		||||
    public Options toOptions() {
 | 
			
		||||
        if (options != null) return options;
 | 
			
		||||
 | 
			
		||||
        return options = new Options(
 | 
			
		||||
 
 | 
			
		||||
@@ -130,7 +130,7 @@ public class HttpRequest extends Resource<HttpRequest> {
 | 
			
		||||
            if (isClosed()) return;
 | 
			
		||||
 | 
			
		||||
            var requestBody = getHeaderSize(headers) + postBuffer.capacity();
 | 
			
		||||
            if (options.maxUpload != 0 && requestBody > options.maxUpload) {
 | 
			
		||||
            if (options.maxUpload() != 0 && requestBody > options.maxUpload()) {
 | 
			
		||||
                failure("Request body is too large");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -136,7 +136,7 @@ public final class HttpRequestHandler extends SimpleChannelInboundHandler<HttpOb
 | 
			
		||||
            var partial = content.content();
 | 
			
		||||
            if (partial.isReadable()) {
 | 
			
		||||
                // If we've read more than we're allowed to handle, abort as soon as possible.
 | 
			
		||||
                if (options.maxDownload != 0 && responseBody.readableBytes() + partial.readableBytes() > options.maxDownload) {
 | 
			
		||||
                if (options.maxDownload() != 0 && responseBody.readableBytes() + partial.readableBytes() > options.maxDownload()) {
 | 
			
		||||
                    closed = true;
 | 
			
		||||
                    ctx.close();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -16,8 +16,8 @@ import java.net.URI;
 | 
			
		||||
 * A version of {@link WebSocketClientHandshaker13} which doesn't add the {@link HttpHeaderNames#ORIGIN} header to the
 | 
			
		||||
 * original HTTP request.
 | 
			
		||||
 */
 | 
			
		||||
public class NoOriginWebSocketHandshaker extends WebSocketClientHandshaker13 {
 | 
			
		||||
    public NoOriginWebSocketHandshaker(URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) {
 | 
			
		||||
class NoOriginWebSocketHandshaker extends WebSocketClientHandshaker13 {
 | 
			
		||||
    NoOriginWebSocketHandshaker(URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) {
 | 
			
		||||
        super(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,9 @@ import dan200.computercraft.core.apis.http.NetworkUtils;
 | 
			
		||||
import dan200.computercraft.core.apis.http.Resource;
 | 
			
		||||
import dan200.computercraft.core.apis.http.ResourceGroup;
 | 
			
		||||
import dan200.computercraft.core.apis.http.options.Options;
 | 
			
		||||
import dan200.computercraft.core.util.IoUtil;
 | 
			
		||||
import dan200.computercraft.core.metrics.Metrics;
 | 
			
		||||
import io.netty.bootstrap.Bootstrap;
 | 
			
		||||
import io.netty.buffer.Unpooled;
 | 
			
		||||
import io.netty.channel.Channel;
 | 
			
		||||
import io.netty.channel.ChannelFuture;
 | 
			
		||||
import io.netty.channel.ChannelInitializer;
 | 
			
		||||
@@ -23,21 +24,22 @@ import io.netty.handler.codec.http.HttpClientCodec;
 | 
			
		||||
import io.netty.handler.codec.http.HttpHeaderNames;
 | 
			
		||||
import io.netty.handler.codec.http.HttpHeaders;
 | 
			
		||||
import io.netty.handler.codec.http.HttpObjectAggregator;
 | 
			
		||||
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
 | 
			
		||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
 | 
			
		||||
import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler;
 | 
			
		||||
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.lang.ref.WeakReference;
 | 
			
		||||
import java.net.URI;
 | 
			
		||||
import java.net.URISyntaxException;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.util.concurrent.Future;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provides functionality to verify and connect to a remote websocket.
 | 
			
		||||
 */
 | 
			
		||||
public class Websocket extends Resource<Websocket> {
 | 
			
		||||
public class Websocket extends Resource<Websocket> implements WebsocketClient {
 | 
			
		||||
    private static final Logger LOG = LoggerFactory.getLogger(Websocket.class);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -46,14 +48,8 @@ public class Websocket extends Resource<Websocket> {
 | 
			
		||||
     */
 | 
			
		||||
    public static final int MAX_MESSAGE_SIZE = 1 << 30;
 | 
			
		||||
 | 
			
		||||
    static final String SUCCESS_EVENT = "websocket_success";
 | 
			
		||||
    static final String FAILURE_EVENT = "websocket_failure";
 | 
			
		||||
    static final String CLOSE_EVENT = "websocket_closed";
 | 
			
		||||
    static final String MESSAGE_EVENT = "websocket_message";
 | 
			
		||||
 | 
			
		||||
    private @Nullable Future<?> executorFuture;
 | 
			
		||||
    private @Nullable ChannelFuture connectFuture;
 | 
			
		||||
    private @Nullable WeakReference<WebsocketHandle> websocketHandle;
 | 
			
		||||
    private @Nullable ChannelFuture channelFuture;
 | 
			
		||||
 | 
			
		||||
    private final IAPIEnvironment environment;
 | 
			
		||||
    private final URI uri;
 | 
			
		||||
@@ -70,38 +66,6 @@ public class Websocket extends Resource<Websocket> {
 | 
			
		||||
        this.timeout = timeout;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static URI checkUri(String address) throws HTTPRequestException {
 | 
			
		||||
        URI uri = null;
 | 
			
		||||
        try {
 | 
			
		||||
            uri = new URI(address);
 | 
			
		||||
        } catch (URISyntaxException ignored) {
 | 
			
		||||
            // Fall through to the case below
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (uri == null || uri.getHost() == null) {
 | 
			
		||||
            try {
 | 
			
		||||
                uri = new URI("ws://" + address);
 | 
			
		||||
            } catch (URISyntaxException ignored) {
 | 
			
		||||
                // Fall through to the case below
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (uri == null || uri.getHost() == null) throw new HTTPRequestException("URL malformed");
 | 
			
		||||
 | 
			
		||||
        var scheme = uri.getScheme();
 | 
			
		||||
        if (scheme == null) {
 | 
			
		||||
            try {
 | 
			
		||||
                uri = new URI("ws://" + uri);
 | 
			
		||||
            } catch (URISyntaxException e) {
 | 
			
		||||
                throw new HTTPRequestException("URL malformed");
 | 
			
		||||
            }
 | 
			
		||||
        } else if (!scheme.equalsIgnoreCase("wss") && !scheme.equalsIgnoreCase("ws")) {
 | 
			
		||||
            throw new HTTPRequestException("Invalid scheme '" + scheme + "'");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return uri;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void connect() {
 | 
			
		||||
        if (isClosed()) return;
 | 
			
		||||
        executorFuture = NetworkUtils.EXECUTOR.submit(this::doConnect);
 | 
			
		||||
@@ -122,7 +86,7 @@ public class Websocket extends Resource<Websocket> {
 | 
			
		||||
            // getAddress may have a slight delay, so let's perform another cancellation check.
 | 
			
		||||
            if (isClosed()) return;
 | 
			
		||||
 | 
			
		||||
            connectFuture = new Bootstrap()
 | 
			
		||||
            channelFuture = new Bootstrap()
 | 
			
		||||
                .group(NetworkUtils.LOOP_GROUP)
 | 
			
		||||
                .channel(NioSocketChannel.class)
 | 
			
		||||
                .handler(new ChannelInitializer<SocketChannel>() {
 | 
			
		||||
@@ -133,7 +97,7 @@ public class Websocket extends Resource<Websocket> {
 | 
			
		||||
                        var subprotocol = headers.get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL);
 | 
			
		||||
                        var handshaker = new NoOriginWebSocketHandshaker(
 | 
			
		||||
                            uri, WebSocketVersion.V13, subprotocol, true, headers,
 | 
			
		||||
                            options.websocketMessage <= 0 ? MAX_MESSAGE_SIZE : options.websocketMessage
 | 
			
		||||
                            options.websocketMessage() <= 0 ? MAX_MESSAGE_SIZE : options.websocketMessage()
 | 
			
		||||
                        );
 | 
			
		||||
 | 
			
		||||
                        var p = ch.pipeline();
 | 
			
		||||
@@ -162,12 +126,12 @@ public class Websocket extends Resource<Websocket> {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void success(Channel channel, Options options) {
 | 
			
		||||
    void success(Options options) {
 | 
			
		||||
        if (isClosed()) return;
 | 
			
		||||
 | 
			
		||||
        var handle = new WebsocketHandle(this, options, channel);
 | 
			
		||||
        var handle = new WebsocketHandle(environment, address, this, options);
 | 
			
		||||
        environment().queueEvent(SUCCESS_EVENT, address, handle);
 | 
			
		||||
        websocketHandle = createOwnerReference(handle);
 | 
			
		||||
        createOwnerReference(handle);
 | 
			
		||||
 | 
			
		||||
        checkClosed();
 | 
			
		||||
    }
 | 
			
		||||
@@ -189,19 +153,35 @@ public class Websocket extends Resource<Websocket> {
 | 
			
		||||
        super.dispose();
 | 
			
		||||
 | 
			
		||||
        executorFuture = closeFuture(executorFuture);
 | 
			
		||||
        connectFuture = closeChannel(connectFuture);
 | 
			
		||||
 | 
			
		||||
        var websocketHandleRef = websocketHandle;
 | 
			
		||||
        var websocketHandle = websocketHandleRef == null ? null : websocketHandleRef.get();
 | 
			
		||||
        IoUtil.closeQuietly(websocketHandle);
 | 
			
		||||
        this.websocketHandle = null;
 | 
			
		||||
        channelFuture = closeChannel(channelFuture);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IAPIEnvironment environment() {
 | 
			
		||||
    IAPIEnvironment environment() {
 | 
			
		||||
        return environment;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public String address() {
 | 
			
		||||
    String address() {
 | 
			
		||||
        return address;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private @Nullable Channel channel() {
 | 
			
		||||
        var channel = channelFuture;
 | 
			
		||||
        return channel == null ? null : channel.channel();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void sendText(String message) {
 | 
			
		||||
        environment.observe(Metrics.WEBSOCKET_OUTGOING, message.length());
 | 
			
		||||
 | 
			
		||||
        var channel = channel();
 | 
			
		||||
        if (channel != null) channel.writeAndFlush(new TextWebSocketFrame(message));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void sendBinary(ByteBuffer message) {
 | 
			
		||||
        environment.observe(Metrics.WEBSOCKET_OUTGOING, message.remaining());
 | 
			
		||||
 | 
			
		||||
        var channel = channel();
 | 
			
		||||
        if (channel != null) channel.writeAndFlush(new BinaryWebSocketFrame(Unpooled.wrappedBuffer(message)));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,90 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis.http.websocket;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.core.apis.http.HTTPRequestException;
 | 
			
		||||
 | 
			
		||||
import java.io.Closeable;
 | 
			
		||||
import java.net.URI;
 | 
			
		||||
import java.net.URISyntaxException;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A client-side websocket, which can be used to send messages to a remote server.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * {@link WebsocketHandle} wraps this into a Lua-compatible interface.
 | 
			
		||||
 */
 | 
			
		||||
public interface WebsocketClient extends Closeable {
 | 
			
		||||
    String SUCCESS_EVENT = "websocket_success";
 | 
			
		||||
    String FAILURE_EVENT = "websocket_failure";
 | 
			
		||||
    String CLOSE_EVENT = "websocket_closed";
 | 
			
		||||
    String MESSAGE_EVENT = "websocket_message";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Determine whether this websocket is closed.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Whether this websocket is closed.
 | 
			
		||||
     */
 | 
			
		||||
    boolean isClosed();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Close this websocket.
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    void close();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send a text websocket frame.
 | 
			
		||||
     *
 | 
			
		||||
     * @param message The message to send.
 | 
			
		||||
     */
 | 
			
		||||
    void sendText(String message);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send a binary websocket frame.
 | 
			
		||||
     *
 | 
			
		||||
     * @param message The message to send.
 | 
			
		||||
     */
 | 
			
		||||
    void sendBinary(ByteBuffer message);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parse an address, ensuring it is a valid websocket URI.
 | 
			
		||||
     *
 | 
			
		||||
     * @param address The address to parse.
 | 
			
		||||
     * @return The parsed URI.
 | 
			
		||||
     * @throws HTTPRequestException If the address is not valid.
 | 
			
		||||
     */
 | 
			
		||||
    static URI parseUri(String address) throws HTTPRequestException {
 | 
			
		||||
        URI uri = null;
 | 
			
		||||
        try {
 | 
			
		||||
            uri = new URI(address);
 | 
			
		||||
        } catch (URISyntaxException ignored) {
 | 
			
		||||
            // Fall through to the case below
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (uri == null || uri.getHost() == null) {
 | 
			
		||||
            try {
 | 
			
		||||
                uri = new URI("ws://" + address);
 | 
			
		||||
            } catch (URISyntaxException ignored) {
 | 
			
		||||
                // Fall through to the case below
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (uri == null || uri.getHost() == null) throw new HTTPRequestException("URL malformed");
 | 
			
		||||
 | 
			
		||||
        var scheme = uri.getScheme();
 | 
			
		||||
        if (scheme == null) {
 | 
			
		||||
            try {
 | 
			
		||||
                uri = new URI("ws://" + uri);
 | 
			
		||||
            } catch (URISyntaxException e) {
 | 
			
		||||
                throw new HTTPRequestException("URL malformed");
 | 
			
		||||
            }
 | 
			
		||||
        } else if (!scheme.equalsIgnoreCase("wss") && !scheme.equalsIgnoreCase("ws")) {
 | 
			
		||||
            throw new HTTPRequestException("Invalid scheme '" + scheme + "'");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return uri;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user