mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-10-19 16:07:38 +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
|
name: Check Java codestyle
|
||||||
files: ".*\\.java$"
|
files: ".*\\.java$"
|
||||||
language: system
|
language: system
|
||||||
entry: ./gradlew checkstyleMain checkstyleTest
|
entry: ./gradlew checkstyle
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
require_serial: true
|
require_serial: true
|
||||||
- id: illuaminate
|
- id: illuaminate
|
||||||
|
14
.reuse/dep5
14
.reuse/dep5
@@ -10,8 +10,8 @@ Files:
|
|||||||
projects/common/src/testMod/resources/data/cctest/structures/*
|
projects/common/src/testMod/resources/data/cctest/structures/*
|
||||||
projects/fabric/src/generated/*
|
projects/fabric/src/generated/*
|
||||||
projects/forge/src/generated/*
|
projects/forge/src/generated/*
|
||||||
projects/web/src/export/index.json
|
projects/web/src/htmlTransform/export/index.json
|
||||||
projects/web/src/export/items/minecraft/*
|
projects/web/src/htmlTransform/export/items/minecraft/*
|
||||||
Comment: Generated/data files are CC0.
|
Comment: Generated/data files are CC0.
|
||||||
Copyright: The CC: Tweaked Developers
|
Copyright: The CC: Tweaked Developers
|
||||||
License: CC0-1.0
|
License: CC0-1.0
|
||||||
@@ -37,10 +37,10 @@ Files:
|
|||||||
projects/fabric/src/testMod/resources/computercraft-gametest.fabric.mixins.json
|
projects/fabric/src/testMod/resources/computercraft-gametest.fabric.mixins.json
|
||||||
projects/fabric/src/testMod/resources/fabric.mod.json
|
projects/fabric/src/testMod/resources/fabric.mod.json
|
||||||
projects/forge/src/client/resources/computercraft-client.forge.mixins.json
|
projects/forge/src/client/resources/computercraft-client.forge.mixins.json
|
||||||
projects/web/src/mount/.settings
|
projects/web/src/frontend/mount/.settings
|
||||||
projects/web/src/mount/example.nfp
|
projects/web/src/frontend/mount/example.nfp
|
||||||
projects/web/src/mount/example.nft
|
projects/web/src/frontend/mount/example.nft
|
||||||
projects/web/src/mount/expr_template.lua
|
projects/web/src/frontend/mount/expr_template.lua
|
||||||
projects/web/tsconfig.json
|
projects/web/tsconfig.json
|
||||||
Comment: Several assets where it's inconvenient to create a .license file.
|
Comment: Several assets where it's inconvenient to create a .license file.
|
||||||
Copyright: The CC: Tweaked Developers
|
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/autorun/.ignoreme
|
||||||
projects/core/src/main/resources/data/computercraft/lua/rom/help/*
|
projects/core/src/main/resources/data/computercraft/lua/rom/help/*
|
||||||
projects/core/src/main/resources/data/computercraft/lua/rom/programs/fun/advanced/levels/*
|
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.
|
Comment: Bulk-license original assets as CCPL.
|
||||||
Copyright: 2011 Daniel Ratcliffe
|
Copyright: 2011 Daniel Ratcliffe
|
||||||
License: LicenseRef-CCPL
|
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"
|
[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."
|
[community]: README.md#community "Get in touch with the community."
|
||||||
[Adoptium]: https://adoptium.net/temurin/releases?version=17 "Download OpenJDK 17"
|
[Adoptium]: https://adoptium.net/temurin/releases?version=17 "Download OpenJDK 17"
|
||||||
[checkstyle]: https://checkstyle.org/
|
|
||||||
[illuaminate]: https://github.com/SquidDev/illuaminate/ "Illuaminate on GitHub"
|
[illuaminate]: https://github.com/SquidDev/illuaminate/ "Illuaminate on GitHub"
|
||||||
[weblate]: https://i18n.tweaked.cc/projects/cc-tweaked/minecraft/ "CC: Tweaked weblate instance"
|
[weblate]: https://i18n.tweaked.cc/projects/cc-tweaked/minecraft/ "CC: Tweaked weblate instance"
|
||||||
[docs]: https://tweaked.cc/ "CC: Tweaked documentation"
|
[docs]: https://tweaked.cc/ "CC: Tweaked documentation"
|
||||||
|
@@ -52,7 +52,7 @@ dependencies {
|
|||||||
implementation(libs.forgeGradle)
|
implementation(libs.forgeGradle)
|
||||||
implementation(libs.librarian)
|
implementation(libs.librarian)
|
||||||
implementation(libs.minotaur)
|
implementation(libs.minotaur)
|
||||||
implementation(libs.quiltflower)
|
implementation(libs.vineflower)
|
||||||
implementation(libs.vanillaGradle)
|
implementation(libs.vanillaGradle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -12,7 +12,7 @@ import cc.tweaked.gradle.MinecraftConfigurations
|
|||||||
plugins {
|
plugins {
|
||||||
`java-library`
|
`java-library`
|
||||||
id("fabric-loom")
|
id("fabric-loom")
|
||||||
id("io.github.juuxel.loom-quiltflower")
|
id("io.github.juuxel.loom-vineflower")
|
||||||
id("cc-tweaked.java-convention")
|
id("cc-tweaked.java-convention")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -66,6 +66,7 @@ repositories {
|
|||||||
includeGroup("me.shedaniel.cloth")
|
includeGroup("me.shedaniel.cloth")
|
||||||
includeGroup("me.shedaniel")
|
includeGroup("me.shedaniel")
|
||||||
includeGroup("mezz.jei")
|
includeGroup("mezz.jei")
|
||||||
|
includeGroup("org.teavm")
|
||||||
includeModule("com.terraformersmc", "modmenu")
|
includeModule("com.terraformersmc", "modmenu")
|
||||||
includeModule("me.lucko", "fabric-permissions-api")
|
includeModule("me.lucko", "fabric-permissions-api")
|
||||||
}
|
}
|
||||||
@@ -99,7 +100,10 @@ sourceSets.all {
|
|||||||
check("FutureReturnValueIgnored", CheckSeverity.OFF) // Too many false positives with Netty
|
check("FutureReturnValueIgnored", CheckSeverity.OFF) // Too many false positives with Netty
|
||||||
|
|
||||||
check("NullAway", CheckSeverity.ERROR)
|
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:ExcludedFieldAnnotations", listOf("org.spongepowered.asm.mixin.Shadow").joinToString(","))
|
||||||
option("NullAway:CastToNonNullMethod", "dan200.computercraft.core.util.Nullability.assertNonNull")
|
option("NullAway:CastToNonNullMethod", "dan200.computercraft.core.util.Nullability.assertNonNull")
|
||||||
option("NullAway:CheckOptionalEmptiness")
|
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 {
|
spotless {
|
||||||
encoding = StandardCharsets.UTF_8
|
encoding = StandardCharsets.UTF_8
|
||||||
lineEndings = LineEnding.UNIX
|
lineEndings = LineEnding.UNIX
|
||||||
@@ -191,6 +201,7 @@ spotless {
|
|||||||
|
|
||||||
val ktlintConfig = mapOf(
|
val ktlintConfig = mapOf(
|
||||||
"ktlint_standard_no-wildcard-imports" to "disabled",
|
"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" to "true",
|
||||||
"ij_kotlin_allow_trailing_comma_on_call_site" to "true",
|
"ij_kotlin_allow_trailing_comma_on_call_site" to "true",
|
||||||
)
|
)
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
package cc.tweaked.gradle
|
package cc.tweaked.gradle
|
||||||
|
|
||||||
|
import org.gradle.api.file.DirectoryProperty
|
||||||
import org.gradle.api.provider.Property
|
import org.gradle.api.provider.Property
|
||||||
import org.gradle.api.tasks.AbstractExecTask
|
import org.gradle.api.tasks.AbstractExecTask
|
||||||
import org.gradle.api.tasks.OutputDirectory
|
import org.gradle.api.tasks.OutputDirectory
|
||||||
@@ -11,5 +12,5 @@ import java.io.File
|
|||||||
|
|
||||||
abstract class ExecToDir : AbstractExecTask<ExecToDir>(ExecToDir::class.java) {
|
abstract class ExecToDir : AbstractExecTask<ExecToDir>(ExecToDir::class.java) {
|
||||||
@get:OutputDirectory
|
@get:OutputDirectory
|
||||||
abstract val output: Property<File>
|
abstract val output: DirectoryProperty
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,8 @@
|
|||||||
package cc.tweaked.gradle
|
package cc.tweaked.gradle
|
||||||
|
|
||||||
import org.gradle.api.artifacts.dsl.DependencyHandler
|
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.Property
|
||||||
import org.gradle.api.provider.Provider
|
import org.gradle.api.provider.Provider
|
||||||
import org.gradle.api.tasks.JavaExec
|
import org.gradle.api.tasks.JavaExec
|
||||||
@@ -124,3 +126,6 @@ class CloseScope : AutoCloseable {
|
|||||||
|
|
||||||
/** Proxy method to avoid overload ambiguity. */
|
/** Proxy method to avoid overload ambiguity. */
|
||||||
fun <T> Property<T>.setProvider(provider: Provider<out T>) = set(provider)
|
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. -->
|
<!-- The commands API is documented in Lua. -->
|
||||||
<suppress checks="SummaryJavadocCheck" files=".*[\\/]CommandAPI.java" />
|
<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>
|
</suppressions>
|
||||||
|
@@ -29,7 +29,7 @@ for _, file in ipairs(files.getFiles()) do
|
|||||||
local size = file.seek("end")
|
local size = file.seek("end")
|
||||||
file.seek("set", 0)
|
file.seek("set", 0)
|
||||||
|
|
||||||
print(file.getName() .. " " .. file.getSize())
|
print(file.getName() .. " " .. size)
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@@ -10,7 +10,7 @@ kotlin.jvm.target.validation.mode=error
|
|||||||
|
|
||||||
# Mod properties
|
# Mod properties
|
||||||
isUnstable=false
|
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
|
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
|
||||||
mcVersion=1.20.1
|
mcVersion=1.20.1
|
||||||
|
@@ -12,12 +12,12 @@ fabric-loader = "0.14.21"
|
|||||||
forge = "47.1.0"
|
forge = "47.1.0"
|
||||||
forgeSpi = "6.0.0"
|
forgeSpi = "6.0.0"
|
||||||
mixin = "0.8.5"
|
mixin = "0.8.5"
|
||||||
parchment = "2023.06.26"
|
parchment = "2023.08.20"
|
||||||
parchmentMc = "1.19.4"
|
parchmentMc = "1.20.1"
|
||||||
|
|
||||||
# Normal dependencies
|
# Normal dependencies
|
||||||
asm = "9.3"
|
asm = "9.5"
|
||||||
autoService = "1.0.1"
|
autoService = "1.1.1"
|
||||||
checkerFramework = "3.32.0"
|
checkerFramework = "3.32.0"
|
||||||
cobalt = "0.7.3"
|
cobalt = "0.7.3"
|
||||||
cobalt-next = "0.7.4" # Not a real version, used to constrain the version we accept.
|
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 = "1.8.10"
|
||||||
kotlin-coroutines = "1.6.4"
|
kotlin-coroutines = "1.6.4"
|
||||||
netty = "4.1.82.Final"
|
netty = "4.1.82.Final"
|
||||||
nightConfig = "3.6.5"
|
nightConfig = "3.6.7"
|
||||||
slf4j = "1.7.36"
|
slf4j = "2.0.1"
|
||||||
|
|
||||||
# Minecraft mods
|
# Minecraft mods
|
||||||
emi = "1.0.8+1.20.1"
|
emi = "1.0.8+1.20.1"
|
||||||
@@ -45,34 +45,36 @@ rubidium = "0.6.1"
|
|||||||
sodium = "mc1.20-0.4.10"
|
sodium = "mc1.20-0.4.10"
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
byteBuddy = "1.14.2"
|
byteBuddy = "1.14.7"
|
||||||
hamcrest = "2.2"
|
hamcrest = "2.2"
|
||||||
jqwik = "1.7.2"
|
jqwik = "1.7.4"
|
||||||
junit = "5.9.2"
|
junit = "5.10.0"
|
||||||
|
|
||||||
# Build tools
|
# Build tools
|
||||||
cctJavadoc = "1.8.0"
|
cctJavadoc = "1.8.0"
|
||||||
checkstyle = "10.3.4"
|
checkstyle = "10.12.3"
|
||||||
curseForgeGradle = "1.0.14"
|
curseForgeGradle = "1.0.14"
|
||||||
errorProne-core = "2.18.0"
|
errorProne-core = "2.21.1"
|
||||||
errorProne-plugin = "3.0.1"
|
errorProne-plugin = "3.1.0"
|
||||||
fabric-loom = "1.3.7"
|
fabric-loom = "1.3.7"
|
||||||
forgeGradle = "6.0.8"
|
forgeGradle = "6.0.8"
|
||||||
githubRelease = "2.2.12"
|
githubRelease = "2.4.1"
|
||||||
ideaExt = "1.1.6"
|
ideaExt = "1.1.7"
|
||||||
illuaminate = "0.1.0-40-g975cbc3"
|
illuaminate = "0.1.0-44-g9ee0055"
|
||||||
librarian = "1.+"
|
librarian = "1.+"
|
||||||
minotaur = "2.+"
|
minotaur = "2.+"
|
||||||
mixinGradle = "0.7.+"
|
mixinGradle = "0.7.+"
|
||||||
nullAway = "0.9.9"
|
nullAway = "0.9.9"
|
||||||
quiltflower = "1.10.0"
|
spotless = "6.21.0"
|
||||||
spotless = "6.17.0"
|
|
||||||
taskTree = "2.1.1"
|
taskTree = "2.1.1"
|
||||||
vanillaGradle = "0.2.1-SNAPSHOT"
|
vanillaGradle = "0.2.1-SNAPSHOT"
|
||||||
|
vineflower = "1.11.0"
|
||||||
|
teavm = "0.9.0-SQUID.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
# Normal dependencies
|
# Normal dependencies
|
||||||
asm = { module = "org.ow2.asm:asm", version.ref = "asm" }
|
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" }
|
autoService = { module = "com.google.auto.service:auto-service", version.ref = "autoService" }
|
||||||
checkerFramework = { module = "org.checkerframework:checker-qual", version.ref = "checkerFramework" }
|
checkerFramework = { module = "org.checkerframework:checker-qual", version.ref = "checkerFramework" }
|
||||||
cobalt = { module = "org.squiddev:Cobalt", version.ref = "cobalt" }
|
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" }
|
librarian = { module = "org.parchmentmc:librarian", version.ref = "librarian" }
|
||||||
minotaur = { module = "com.modrinth.minotaur:Minotaur", version.ref = "minotaur" }
|
minotaur = { module = "com.modrinth.minotaur:Minotaur", version.ref = "minotaur" }
|
||||||
nullAway = { module = "com.uber.nullaway:nullaway", version.ref = "nullAway" }
|
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" }
|
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" }
|
vanillaGradle = { module = "org.spongepowered:vanillagradle", version.ref = "vanillaGradle" }
|
||||||
|
vineflower = { module = "io.github.juuxel:loom-vineflower", version.ref = "vineflower" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
forgeGradle = { id = "net.minecraftforge.gradle", version.ref = "forgeGradle" }
|
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" }
|
taskTree = { id = "com.dorongold.task-tree", version.ref = "taskTree" }
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
|
annotations = ["jsr305", "checkerFramework", "jetbrainsAnnotations"]
|
||||||
kotlin = ["kotlin-stdlib", "kotlin-coroutines"]
|
kotlin = ["kotlin-stdlib", "kotlin-coroutines"]
|
||||||
|
|
||||||
# Minecraft
|
# Minecraft
|
||||||
@@ -164,3 +175,7 @@ externalMods-fabric-runtime = ["jei-fabric", "modmenu"]
|
|||||||
# Testing
|
# Testing
|
||||||
test = ["junit-jupiter-api", "junit-jupiter-params", "hamcrest", "jqwik-api"]
|
test = ["junit-jupiter-api", "junit-jupiter-params", "hamcrest", "jqwik-api"]
|
||||||
testRuntime = ["junit-jupiter-engine", "jqwik-engine"]
|
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/bios.lua
|
||||||
/projects/core/src/main/resources/data/computercraft/lua/rom/
|
/projects/core/src/main/resources/data/computercraft/lua/rom/
|
||||||
/projects/core/src/test/resources/test-rom
|
/projects/core/src/test/resources/test-rom
|
||||||
/projects/web/src/mount)
|
/projects/web/src/frontend/mount)
|
||||||
|
|
||||||
(doc
|
(doc
|
||||||
; Also defined in projects/web/build.gradle.kts
|
; Also defined in projects/web/build.gradle.kts
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
(url https://tweaked.cc/)
|
(url https://tweaked.cc/)
|
||||||
(source-link https://github.com/cc-tweaked/CC-Tweaked/blob/${commit}/${path}#L${line})
|
(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)
|
(scripts /projects/web/build/rollup/index.js)
|
||||||
(head doc/head.html))
|
(head doc/head.html))
|
||||||
|
|
||||||
@@ -115,4 +115,4 @@
|
|||||||
:max sleep write
|
:max sleep write
|
||||||
cct_test describe expect howlci fail it pending stub before_each)))
|
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",
|
"license": "BSD-3-Clause",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@squid-dev/cc-web-term": "^2.0.0",
|
||||||
"preact": "^10.5.5",
|
"preact": "^10.5.5",
|
||||||
|
"setimmediate": "^1.0.5",
|
||||||
"tslib": "^2.0.3"
|
"tslib": "^2.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-terser": "^0.4.0",
|
"@rollup/plugin-node-resolve": "^15.2.1",
|
||||||
"@rollup/plugin-typescript": "^11.0.0",
|
"@rollup/plugin-typescript": "^11.0.0",
|
||||||
"@rollup/plugin-url": "^8.0.1",
|
"@rollup/plugin-url": "^8.0.1",
|
||||||
"@types/glob": "^8.1.0",
|
"@swc/core": "^1.3.92",
|
||||||
"@types/react-dom": "^18.0.5",
|
"@types/node": "^20.8.3",
|
||||||
"glob": "^9.3.0",
|
"lightningcss": "^1.22.0",
|
||||||
"react-dom": "^18.1.0",
|
"preact-render-to-string": "^6.2.1",
|
||||||
"react": "^18.1.0",
|
"rehype": "^13.0.0",
|
||||||
"rehype-highlight": "^6.0.0",
|
"rehype-highlight": "^7.0.0",
|
||||||
"rehype-react": "^7.1.1",
|
"rehype-react": "^8.0.0",
|
||||||
"rehype": "^12.0.1",
|
"rollup": "^4.0.0",
|
||||||
"requirejs": "^2.3.6",
|
"tsx": "^3.12.10",
|
||||||
"rollup": "^3.19.1",
|
"typescript": "^5.2.2"
|
||||||
"ts-node": "^10.8.0",
|
|
||||||
"typescript": "^4.0.5"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -17,7 +17,7 @@ import org.joml.Matrix4f;
|
|||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
class TurtleUpgradeModellers {
|
final class TurtleUpgradeModellers {
|
||||||
private static final Transformation leftTransform = getMatrixFor(-0.4065f);
|
private static final Transformation leftTransform = getMatrixFor(-0.4065f);
|
||||||
private static final Transformation rightTransform = 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();
|
static final TurtleUpgradeModeller<ITurtleUpgrade> UPGRADE_ITEM = new UpgradeItemModeller();
|
||||||
|
|
||||||
private static class UpgradeItemModeller implements TurtleUpgradeModeller<ITurtleUpgrade> {
|
private static final class UpgradeItemModeller implements TurtleUpgradeModeller<ITurtleUpgrade> {
|
||||||
@Override
|
@Override
|
||||||
public TransformedModel getModel(ITurtleUpgrade upgrade, @Nullable ITurtleAccess turtle, TurtleSide side) {
|
public TransformedModel getModel(ITurtleUpgrade upgrade, @Nullable ITurtleAccess turtle, TurtleSide side) {
|
||||||
return getModel(turtle == null ? upgrade.getCraftingItem() : upgrade.getUpgradeItem(turtle.getUpgradeNBTData(side)), 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> WIRED_MODEM = make("wired_modem");
|
||||||
public static final TagKey<Block> MONITOR = make("monitor");
|
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.
|
* Blocks which can be broken by any turtle tool.
|
||||||
*/
|
*/
|
||||||
|
@@ -39,4 +39,6 @@ dependencies {
|
|||||||
testModImplementation(testFixtures(project(":core")))
|
testModImplementation(testFixtures(project(":core")))
|
||||||
testModImplementation(testFixtures(project(":common")))
|
testModImplementation(testFixtures(project(":common")))
|
||||||
testModImplementation(libs.bundles.kotlin)
|
testModImplementation(libs.bundles.kotlin)
|
||||||
|
|
||||||
|
testFixturesImplementation(testFixtures(project(":core")))
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
// SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
// SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: LicenseRef-CCPL
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package dan200.computercraft.client.gui;
|
package dan200.computercraft.client.gui;
|
||||||
|
|
||||||
|
@@ -47,7 +47,7 @@ public class ShaderMod {
|
|||||||
Optional<ShaderMod> get();
|
Optional<ShaderMod> get();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class Storage {
|
private static final class Storage {
|
||||||
static final ShaderMod INSTANCE = ServiceLoader.load(Provider.class)
|
static final ShaderMod INSTANCE = ServiceLoader.load(Provider.class)
|
||||||
.stream()
|
.stream()
|
||||||
.flatMap(x -> x.get().get().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.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.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.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.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.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");
|
add("commands.computercraft.view.action", "View this computer");
|
||||||
|
@@ -5,6 +5,7 @@
|
|||||||
package dan200.computercraft.data;
|
package dan200.computercraft.data;
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
|
import com.mojang.authlib.GameProfile;
|
||||||
import dan200.computercraft.api.ComputerCraftAPI;
|
import dan200.computercraft.api.ComputerCraftAPI;
|
||||||
import dan200.computercraft.api.pocket.PocketUpgradeDataProvider;
|
import dan200.computercraft.api.pocket.PocketUpgradeDataProvider;
|
||||||
import dan200.computercraft.api.turtle.TurtleUpgradeDataProvider;
|
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.PackOutput;
|
||||||
import net.minecraft.data.recipes.*;
|
import net.minecraft.data.recipes.*;
|
||||||
import net.minecraft.nbt.CompoundTag;
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.nbt.NbtUtils;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
import net.minecraft.tags.TagKey;
|
import net.minecraft.tags.TagKey;
|
||||||
import net.minecraft.util.GsonHelper;
|
import net.minecraft.util.GsonHelper;
|
||||||
import net.minecraft.world.item.DyeColor;
|
import net.minecraft.world.item.*;
|
||||||
import net.minecraft.world.item.DyeItem;
|
|
||||||
import net.minecraft.world.item.Item;
|
|
||||||
import net.minecraft.world.item.Items;
|
|
||||||
import net.minecraft.world.item.crafting.Ingredient;
|
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.ShapedRecipe;
|
||||||
import net.minecraft.world.item.crafting.SimpleCraftingRecipeSerializer;
|
import net.minecraft.world.item.crafting.SimpleCraftingRecipeSerializer;
|
||||||
import net.minecraft.world.level.ItemLike;
|
import net.minecraft.world.level.ItemLike;
|
||||||
@@ -41,6 +39,7 @@ import net.minecraft.world.level.block.Blocks;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import static dan200.computercraft.api.ComputerCraftTags.Items.COMPUTER;
|
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())
|
.requires(ModRegistry.Items.MONITOR_NORMAL.get())
|
||||||
.unlockedBy("has_monitor", inventoryChange(ModRegistry.Items.MONITOR_NORMAL.get()))
|
.unlockedBy("has_monitor", inventoryChange(ModRegistry.Items.MONITOR_NORMAL.get()))
|
||||||
.save(
|
.save(
|
||||||
RecipeWrapper.wrap(RecipeSerializer.SHAPELESS_RECIPE, add)
|
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.SHAPELESS.get(), add)
|
||||||
.withResultTag(playerHead("Cloudhunter", "6d074736-b1e9-4378-a99b-bd8777821c9c")),
|
.withResultTag(playerHead("Cloudhunter", "6d074736-b1e9-4378-a99b-bd8777821c9c")),
|
||||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "skull_cloudy")
|
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())
|
.requires(ModRegistry.Items.COMPUTER_ADVANCED.get())
|
||||||
.unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_ADVANCED.get()))
|
.unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_ADVANCED.get()))
|
||||||
.save(
|
.save(
|
||||||
RecipeWrapper.wrap(RecipeSerializer.SHAPELESS_RECIPE, add)
|
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.SHAPELESS.get(), add)
|
||||||
.withResultTag(playerHead("dan200", "f3c8d69b-0776-4512-8434-d1b2165909eb")),
|
.withResultTag(playerHead("dan200", "f3c8d69b-0776-4512-8434-d1b2165909eb")),
|
||||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "skull_dan200")
|
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) {
|
private static CompoundTag playerHead(String name, String uuid) {
|
||||||
var owner = new CompoundTag();
|
var owner = NbtUtils.writeGameProfile(new CompoundTag(), new GameProfile(UUID.fromString(uuid), name));
|
||||||
owner.putString("Name", name);
|
|
||||||
owner.putString("Id", uuid);
|
|
||||||
|
|
||||||
var tag = new CompoundTag();
|
var tag = new CompoundTag();
|
||||||
tag.put("SkullOwner", owner);
|
tag.put(PlayerHeadItem.TAG_SKULL_OWNER, owner);
|
||||||
return tag;
|
return tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Consumer<JsonObject> family(ComputerFamily family) {
|
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) {
|
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.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.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(
|
tags.tag(ComputerCraftTags.Blocks.TURTLE_ALWAYS_BREAKABLE).addTag(BlockTags.LEAVES).add(
|
||||||
Blocks.BAMBOO, Blocks.BAMBOO_SAPLING // Bamboo isn't instabreak for some odd reason.
|
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);
|
nodes.add(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
private WiredNetworkImpl(HashSet<WiredNodeImpl> nodes) {
|
private WiredNetworkImpl(Set<WiredNodeImpl> nodes) {
|
||||||
this.nodes = 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<>();
|
Queue<WiredNodeImpl> enqueued = new ArrayDeque<>();
|
||||||
var reachable = new HashSet<WiredNodeImpl>();
|
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.ColourableRecipe;
|
||||||
import dan200.computercraft.shared.common.DefaultBundledRedstoneProvider;
|
import dan200.computercraft.shared.common.DefaultBundledRedstoneProvider;
|
||||||
import dan200.computercraft.shared.common.HeldItemMenu;
|
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.CommandComputerBlockEntity;
|
||||||
import dan200.computercraft.shared.computer.blocks.ComputerBlock;
|
import dan200.computercraft.shared.computer.blocks.ComputerBlock;
|
||||||
import dan200.computercraft.shared.computer.blocks.ComputerBlockEntity;
|
import dan200.computercraft.shared.computer.blocks.ComputerBlockEntity;
|
||||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||||
import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory;
|
import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory;
|
||||||
import dan200.computercraft.shared.computer.inventory.ViewComputerMenu;
|
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.items.ComputerItem;
|
||||||
import dan200.computercraft.shared.computer.recipe.ComputerUpgradeRecipe;
|
import dan200.computercraft.shared.computer.recipe.ComputerUpgradeRecipe;
|
||||||
import dan200.computercraft.shared.data.BlockNamedEntityLootCondition;
|
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.PocketModem;
|
||||||
import dan200.computercraft.shared.pocket.peripherals.PocketSpeaker;
|
import dan200.computercraft.shared.pocket.peripherals.PocketSpeaker;
|
||||||
import dan200.computercraft.shared.pocket.recipes.PocketComputerUpgradeRecipe;
|
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.FurnaceRefuelHandler;
|
||||||
import dan200.computercraft.shared.turtle.blocks.TurtleBlock;
|
import dan200.computercraft.shared.turtle.blocks.TurtleBlock;
|
||||||
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
|
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.TurtleRecipe;
|
||||||
import dan200.computercraft.shared.turtle.recipes.TurtleUpgradeRecipe;
|
import dan200.computercraft.shared.turtle.recipes.TurtleUpgradeRecipe;
|
||||||
import dan200.computercraft.shared.turtle.upgrades.*;
|
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.CommandSourceStack;
|
||||||
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
|
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
|
||||||
import net.minecraft.commands.synchronization.SingletonArgumentInfo;
|
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",
|
public static final RegistryEntry<ComputerBlock<ComputerBlockEntity>> COMPUTER_ADVANCED = REGISTRY.register("computer_advanced",
|
||||||
() -> new ComputerBlock<>(computerProperties().mapColor(MapColor.GOLD), ComputerFamily.ADVANCED, BlockEntities.COMPUTER_ADVANCED));
|
() -> new ComputerBlock<>(computerProperties().mapColor(MapColor.GOLD), 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),
|
computerProperties().strength(-1, 6000000.0F),
|
||||||
ComputerFamily.COMMAND, BlockEntities.COMPUTER_COMMAND
|
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_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_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",
|
public static final RegistryEntry<PocketComputerItem> POCKET_COMPUTER_NORMAL = REGISTRY.register("pocket_computer_normal",
|
||||||
() -> new PocketComputerItem(properties().stacksTo(1), ComputerFamily.NORMAL));
|
() -> new PocketComputerItem(properties().stacksTo(1), ComputerFamily.NORMAL));
|
||||||
@@ -357,17 +361,21 @@ public final class ModRegistry {
|
|||||||
return REGISTRY.register(name, () -> new SimpleCraftingRecipeSerializer<>(factory));
|
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<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<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<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<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<PrintoutRecipe>> PRINTOUT = simple("printout", PrintoutRecipe::new);
|
||||||
public static final RegistryEntry<SimpleCraftingRecipeSerializer<DiskRecipe>> DISK = simple("disk", DiskRecipe::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<RecipeSerializer<ComputerUpgradeRecipe>> 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 class Permissions {
|
public static class Permissions {
|
||||||
|
@@ -26,7 +26,6 @@ import dan200.computercraft.shared.network.container.ComputerContainerData;
|
|||||||
import net.minecraft.commands.CommandSourceStack;
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
|
||||||
import net.minecraft.world.MenuProvider;
|
import net.minecraft.world.MenuProvider;
|
||||||
import net.minecraft.world.entity.RelativeMovement;
|
import net.minecraft.world.entity.RelativeMovement;
|
||||||
import net.minecraft.world.entity.player.Inventory;
|
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.inventory.AbstractContainerMenu;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.minecraft.world.level.Level;
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
import static dan200.computercraft.shared.command.CommandUtils.isPlayer;
|
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.getComputerArgument;
|
||||||
import static dan200.computercraft.shared.command.arguments.ComputerArgumentType.oneComputer;
|
import static dan200.computercraft.shared.command.arguments.ComputerArgumentType.oneComputer;
|
||||||
import static dan200.computercraft.shared.command.arguments.ComputersArgumentType.*;
|
import static dan200.computercraft.shared.command.arguments.ComputersArgumentType.*;
|
||||||
@@ -62,118 +63,25 @@ public final class CommandComputerCraft {
|
|||||||
dispatcher.register(choice("computercraft")
|
dispatcher.register(choice("computercraft")
|
||||||
.then(literal("dump")
|
.then(literal("dump")
|
||||||
.requires(ModRegistry.Permissions.PERMISSION_DUMP)
|
.requires(ModRegistry.Permissions.PERMISSION_DUMP)
|
||||||
.executes(context -> {
|
.executes(c -> dump(c.getSource()))
|
||||||
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();
|
|
||||||
})
|
|
||||||
.then(args()
|
.then(args()
|
||||||
.arg("computer", oneComputer())
|
.arg("computer", oneComputer())
|
||||||
.executes(context -> {
|
.executes(c -> dumpComputer(c.getSource(), getComputerArgument(c, "computer")))))
|
||||||
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;
|
|
||||||
})))
|
|
||||||
|
|
||||||
.then(command("shutdown")
|
.then(command("shutdown")
|
||||||
.requires(ModRegistry.Permissions.PERMISSION_SHUTDOWN)
|
.requires(ModRegistry.Permissions.PERMISSION_SHUTDOWN)
|
||||||
.argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers())
|
.argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers())
|
||||||
.executes((context, computerSelectors) -> {
|
.executes((c, a) -> shutdown(c.getSource(), unwrap(c.getSource(), a))))
|
||||||
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;
|
|
||||||
}))
|
|
||||||
|
|
||||||
.then(command("turn-on")
|
.then(command("turn-on")
|
||||||
.requires(ModRegistry.Permissions.PERMISSION_TURN_ON)
|
.requires(ModRegistry.Permissions.PERMISSION_TURN_ON)
|
||||||
.argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers())
|
.argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers())
|
||||||
.executes((context, computerSelectors) -> {
|
.executes((c, a) -> turnOn(c.getSource(), unwrap(c.getSource(), a))))
|
||||||
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;
|
|
||||||
}))
|
|
||||||
|
|
||||||
.then(command("tp")
|
.then(command("tp")
|
||||||
.requires(ModRegistry.Permissions.PERMISSION_TP)
|
.requires(ModRegistry.Permissions.PERMISSION_TP)
|
||||||
.arg("computer", oneComputer())
|
.arg("computer", oneComputer())
|
||||||
.executes(context -> {
|
.executes(c -> teleport(c.getSource(), getComputerArgument(c, "computer"))))
|
||||||
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;
|
|
||||||
}))
|
|
||||||
|
|
||||||
.then(command("queue")
|
.then(command("queue")
|
||||||
.requires(ModRegistry.Permissions.PERMISSION_QUEUE)
|
.requires(ModRegistry.Permissions.PERMISSION_QUEUE)
|
||||||
@@ -182,79 +90,243 @@ public final class CommandComputerCraft {
|
|||||||
.suggests((context, builder) -> Suggestions.empty())
|
.suggests((context, builder) -> Suggestions.empty())
|
||||||
)
|
)
|
||||||
.argManyValue("args", StringArgumentType.string(), Collections.emptyList())
|
.argManyValue("args", StringArgumentType.string(), Collections.emptyList())
|
||||||
.executes((ctx, args) -> {
|
.executes((c, a) -> queue(getComputersArgument(c, "computer"), a)))
|
||||||
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;
|
|
||||||
}))
|
|
||||||
|
|
||||||
.then(command("view")
|
.then(command("view")
|
||||||
.requires(ModRegistry.Permissions.PERMISSION_VIEW)
|
.requires(ModRegistry.Permissions.PERMISSION_VIEW)
|
||||||
.arg("computer", oneComputer())
|
.arg("computer", oneComputer())
|
||||||
.executes(context -> {
|
.executes(c -> view(c.getSource(), getComputerArgument(c, "computer"))))
|
||||||
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;
|
|
||||||
}))
|
|
||||||
|
|
||||||
.then(choice("track")
|
.then(choice("track")
|
||||||
.requires(ModRegistry.Permissions.PERMISSION_TRACK)
|
.requires(ModRegistry.Permissions.PERMISSION_TRACK)
|
||||||
.then(command("start")
|
.then(command("start").executes(c -> trackStart(c.getSource())))
|
||||||
.executes(context -> {
|
.then(command("stop").executes(c -> trackStop(c.getSource())))
|
||||||
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("dump")
|
.then(command("dump")
|
||||||
.argManyValue("fields", metric(), DEFAULT_FIELDS)
|
.argManyValue("fields", metric(), DEFAULT_FIELDS)
|
||||||
.executes((context, fields) -> {
|
.executes((c, f) -> trackDump(c.getSource(), f))))
|
||||||
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);
|
|
||||||
})))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
private static Component linkComputer(CommandSourceStack source, @Nullable ServerComputer serverComputer, int computerId) {
|
||||||
var out = Component.literal("");
|
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);
|
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 {
|
private static int displayTimings(CommandSourceStack source, List<ComputerMetrics> timings, AggregatedMetric sortField, List<AggregatedMetric> fields) throws CommandSyntaxException {
|
||||||
if (timings.isEmpty()) throw NO_TIMINGS_EXCEPTION.create();
|
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 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 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");
|
public static final SimpleCommandExceptionType ARGUMENT_EXPECTED = translated("argument.computercraft.argument_expected");
|
||||||
|
|
||||||
private static SimpleCommandExceptionType translated(String key) {
|
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()) {
|
if (server == null || !server.isCommandBlockEnabled()) {
|
||||||
player.displayClientMessage(Component.translatable("advMode.notEnabled"), true);
|
player.displayClientMessage(Component.translatable("advMode.notEnabled"), true);
|
||||||
return false;
|
return false;
|
||||||
} else if (Config.commandRequireCreative ? !player.canUseGameMasterBlocks() : !server.getPlayerList().isOp(player.getGameProfile())) {
|
} else if (!canUseCommandBlock(player)) {
|
||||||
player.displayClientMessage(Component.translatable("advMode.notAllowed"), true);
|
player.displayClientMessage(Component.translatable("advMode.notAllowed"), true);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
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;
|
package dan200.computercraft.shared.computer.core;
|
||||||
|
|
||||||
public enum ComputerFamily {
|
import com.google.gson.JsonObject;
|
||||||
NORMAL,
|
import com.google.gson.JsonSyntaxException;
|
||||||
ADVANCED,
|
import net.minecraft.util.GsonHelper;
|
||||||
COMMAND
|
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> {
|
public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ResourceMount.class);
|
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.
|
* 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;
|
var hasAny = false;
|
||||||
String existingNamespace = null;
|
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()) {
|
for (var file : manager.listResources(subPath, s -> true).keySet()) {
|
||||||
existingNamespace = file.getNamespace();
|
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?
|
if (!FileSystem.contains(subPath, file.getPath())) continue; // Some packs seem to include the parent?
|
||||||
|
|
||||||
var localPath = FileSystem.toLocal(file.getPath(), subPath);
|
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;
|
hasAny = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,65 +85,24 @@ public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void create(FileEntry lastEntry, String path) {
|
private FileEntry createEntry(String path) {
|
||||||
var lastIndex = 0;
|
return new FileEntry(new ResourceLocation(namespace, subPath + "/" + path));
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getSize(FileEntry file) {
|
protected byte[] getFileContents(String path, FileEntry file) throws IOException {
|
||||||
var resource = manager.getResource(file.identifier).orElse(null);
|
var resource = manager.getResource(file.identifier).orElse(null);
|
||||||
if (resource == null) return 0;
|
if (resource == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
try (var stream = resource.open()) {
|
try (var stream = resource.open()) {
|
||||||
return stream.readAllBytes();
|
return stream.readAllBytes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static class FileEntry extends ArchiveMount.FileEntry<FileEntry> {
|
protected static final class FileEntry extends ArchiveMount.FileEntry<FileEntry> {
|
||||||
final ResourceLocation identifier;
|
final ResourceLocation identifier;
|
||||||
|
|
||||||
FileEntry(String path, ResourceLocation identifier) {
|
FileEntry(ResourceLocation identifier) {
|
||||||
super(path);
|
|
||||||
this.identifier = identifier;
|
this.identifier = identifier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -64,13 +64,7 @@ public abstract class AbstractComputerItem extends BlockItem implements ICompute
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable Mount createDataMount(ItemStack stack, ServerLevel level) {
|
public @Nullable Mount createDataMount(ItemStack stack, ServerLevel level) {
|
||||||
var family = getFamily();
|
var id = getComputerID(stack);
|
||||||
if (family != ComputerFamily.COMMAND) {
|
return id >= 0 ? ComputerCraftAPI.createSaveDirMount(level.getServer(), "computer/" + id, Config.computerSpaceLimit) : null;
|
||||||
var id = getComputerID(stack);
|
|
||||||
if (id >= 0) {
|
|
||||||
return ComputerCraftAPI.createSaveDirMount(level.getServer(), "computer/" + id, Config.computerSpaceLimit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 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;
|
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.network.client.UploadResultMessage;
|
||||||
import dan200.computercraft.shared.platform.PlatformHelper;
|
import dan200.computercraft.shared.platform.PlatformHelper;
|
||||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||||
@@ -18,7 +23,6 @@ import org.slf4j.LoggerFactory;
|
|||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default concrete implementation of {@link ServerInputHandler}.
|
* 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[]{
|
computer.queueEvent(TransferredFiles.EVENT, new Object[]{
|
||||||
new TransferredFiles(player, owner, toUpload.stream().map(x -> new TransferredFile(x.getName(), x.getBytes())).collect(Collectors.toList())),
|
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);
|
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 value = new AtomicLong();
|
||||||
final AtomicLong count = new AtomicLong();
|
final AtomicLong count = new AtomicLong();
|
||||||
}
|
}
|
||||||
|
@@ -5,31 +5,20 @@
|
|||||||
package dan200.computercraft.shared.computer.recipe;
|
package dan200.computercraft.shared.computer.recipe;
|
||||||
|
|
||||||
import dan200.computercraft.shared.computer.items.IComputerItem;
|
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.core.RegistryAccess;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
import net.minecraft.world.inventory.CraftingContainer;
|
import net.minecraft.world.inventory.CraftingContainer;
|
||||||
import net.minecraft.world.item.ItemStack;
|
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;
|
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 {
|
public abstract class ComputerConvertRecipe extends CustomShapedRecipe {
|
||||||
private final String group;
|
public ComputerConvertRecipe(ResourceLocation identifier, ShapedRecipeSpec recipe) {
|
||||||
private final ItemStack result;
|
super(identifier, recipe);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract ItemStack convert(IComputerItem item, ItemStack stack);
|
protected abstract ItemStack convert(IComputerItem item, ItemStack stack);
|
||||||
@@ -55,9 +44,4 @@ public abstract class ComputerConvertRecipe extends ShapedRecipe {
|
|||||||
|
|
||||||
return ItemStack.EMPTY;
|
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;
|
package dan200.computercraft.shared.computer.recipe;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
import dan200.computercraft.shared.ModRegistry;
|
import dan200.computercraft.shared.ModRegistry;
|
||||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||||
import dan200.computercraft.shared.computer.items.IComputerItem;
|
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.resources.ResourceLocation;
|
||||||
import net.minecraft.world.item.ItemStack;
|
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.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) {
|
* A recipe which "upgrades" a {@linkplain IComputerItem computer}, converting it from one {@linkplain ComputerFamily
|
||||||
super(identifier, group, category, width, height, ingredients, result, family);
|
* 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
|
@Override
|
||||||
protected ItemStack convert(IComputerItem item, ItemStack stack) {
|
protected ItemStack convert(IComputerItem item, ItemStack stack) {
|
||||||
return item.withFamily(stack, getFamily());
|
return item.withFamily(stack, family);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RecipeSerializer<?> getSerializer() {
|
public RecipeSerializer<ComputerUpgradeRecipe> getSerializer() {
|
||||||
return ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get();
|
return ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Serializer extends ComputerFamilyRecipe.Serializer<ComputerUpgradeRecipe> {
|
public static class Serializer implements RecipeSerializer<ComputerUpgradeRecipe> {
|
||||||
@Override
|
@Override
|
||||||
protected ComputerUpgradeRecipe create(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family) {
|
public ComputerUpgradeRecipe fromJson(ResourceLocation identifier, JsonObject json) {
|
||||||
return new ComputerUpgradeRecipe(identifier, group, category, width, height, ingredients, result, family);
|
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.
|
* 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
|
* @see ItemStack#getTooltip
|
||||||
*/
|
*/
|
||||||
var hideFlags = tag != null ? tag.getInt("HideFlags") : 0;
|
var hideFlags = tag != null ? tag.getInt("HideFlags") : 0;
|
||||||
@@ -116,6 +116,7 @@ public class ItemDetails {
|
|||||||
* @param enchants The enchantment map to add it to.
|
* @param enchants The enchantment map to add it to.
|
||||||
* @see EnchantmentHelper
|
* @see EnchantmentHelper
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("NonApiType")
|
||||||
private static void addEnchantments(ListTag rawEnchants, ArrayList<Map<String, Object>> enchants) {
|
private static void addEnchantments(ListTag rawEnchants, ArrayList<Map<String, Object>> enchants) {
|
||||||
if (rawEnchants.isEmpty()) return;
|
if (rawEnchants.isEmpty()) return;
|
||||||
|
|
||||||
|
@@ -69,7 +69,7 @@ public abstract class PermissionRegistry {
|
|||||||
.orElseGet(DefaultPermissionRegistry::new);
|
.orElseGet(DefaultPermissionRegistry::new);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class DefaultPermissionRegistry extends PermissionRegistry {
|
private static final class DefaultPermissionRegistry extends PermissionRegistry {
|
||||||
@Override
|
@Override
|
||||||
public Predicate<CommandSourceStack> registerCommand(String command, UserLevel fallback) {
|
public Predicate<CommandSourceStack> registerCommand(String command, UserLevel fallback) {
|
||||||
checkNotFrozen();
|
checkNotFrozen();
|
||||||
|
@@ -35,7 +35,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||||||
public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
|
public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
|
||||||
private static final String NBT_ITEM = "Item";
|
private static final String NBT_ITEM = "Item";
|
||||||
|
|
||||||
private static class MountInfo {
|
private static final class MountInfo {
|
||||||
@Nullable
|
@Nullable
|
||||||
String mountPath;
|
String mountPath;
|
||||||
}
|
}
|
||||||
|
@@ -36,7 +36,7 @@ import java.util.Collections;
|
|||||||
public class CableBlockEntity extends BlockEntity {
|
public class CableBlockEntity extends BlockEntity {
|
||||||
private static final String NBT_PERIPHERAL_ENABLED = "PeripheralAccess";
|
private static final String NBT_PERIPHERAL_ENABLED = "PeripheralAccess";
|
||||||
|
|
||||||
private class CableElement extends WiredModemElement {
|
private final class CableElement extends WiredModemElement {
|
||||||
@Override
|
@Override
|
||||||
public Level getLevel() {
|
public Level getLevel() {
|
||||||
return CableBlockEntity.this.getLevel();
|
return CableBlockEntity.this.getLevel();
|
||||||
|
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
package dan200.computercraft.shared.peripheral.modem.wired;
|
package dan200.computercraft.shared.peripheral.modem.wired;
|
||||||
|
|
||||||
|
import dan200.computercraft.api.ComputerCraftTags;
|
||||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||||
import dan200.computercraft.shared.ModRegistry;
|
|
||||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||||
import dan200.computercraft.shared.platform.ComponentAccess;
|
import dan200.computercraft.shared.platform.ComponentAccess;
|
||||||
import dan200.computercraft.shared.platform.PlatformHelper;
|
import dan200.computercraft.shared.platform.PlatformHelper;
|
||||||
@@ -124,8 +124,7 @@ public final class WiredModemLocalPeripheral {
|
|||||||
private IPeripheral getPeripheralFrom(Level world, BlockPos pos, Direction direction) {
|
private IPeripheral getPeripheralFrom(Level world, BlockPos pos, Direction direction) {
|
||||||
var offset = pos.relative(direction);
|
var offset = pos.relative(direction);
|
||||||
|
|
||||||
var block = world.getBlockState(offset).getBlock();
|
if (world.getBlockState(offset).is(ComputerCraftTags.Blocks.PERIPHERAL_HUB_IGNORE)) return null;
|
||||||
if (block == ModRegistry.Blocks.WIRED_MODEM_FULL.get() || block == ModRegistry.Blocks.CABLE.get()) return null;
|
|
||||||
|
|
||||||
var peripheral = peripherals.get((ServerLevel) world, pos, direction);
|
var peripheral = peripherals.get((ServerLevel) world, pos, direction);
|
||||||
return peripheral instanceof WiredModemPeripheral ? null : peripheral;
|
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.
|
// Either delete the current monitor or sync a new one.
|
||||||
if (needsTerminal) {
|
if (needsTerminal) {
|
||||||
if (serverMonitor == null) serverMonitor = new ServerMonitor(advanced, this);
|
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
|
// Update the terminal's width and height and rebuild it. This ensures the monitor
|
||||||
// is consistent when syncing it to other monitors.
|
// is consistent when syncing it to other monitors.
|
||||||
if (serverMonitor != null) serverMonitor.rebuild();
|
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
|
// Update the other monitors, setting coordinates, dimensions and the server terminal
|
||||||
var pos = getBlockPos();
|
var pos = getBlockPos();
|
||||||
|
@@ -55,6 +55,12 @@ public class ServerMonitor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized void reset() {
|
||||||
|
if (terminal == null) return;
|
||||||
|
terminal = null;
|
||||||
|
markChanged();
|
||||||
|
}
|
||||||
|
|
||||||
private void markChanged() {
|
private void markChanged() {
|
||||||
if (!changed.getAndSet(true)) TickScheduler.schedule(origin.tickToken);
|
if (!changed.getAndSet(true)) TickScheduler.schedule(origin.tickToken);
|
||||||
}
|
}
|
||||||
|
@@ -188,7 +188,7 @@ public abstract class SpeakerPeripheral implements IPeripheral {
|
|||||||
* {@literal false}.
|
* {@literal false}.
|
||||||
* <p>
|
* <p>
|
||||||
* ### Valid instruments
|
* ### 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:
|
* These are:
|
||||||
* <p>
|
* <p>
|
||||||
* {@code "harp"}, {@code "basedrum"}, {@code "snare"}, {@code "hat"}, {@code "bass"}, {@code "flute"},
|
* {@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.
|
* Plays a Minecraft sound through the speaker.
|
||||||
* <p>
|
* <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.
|
* {@code "minecraft:block.note_block.harp"}, as well as an optional volume and pitch.
|
||||||
* <p>
|
* <p>
|
||||||
* Only one sound can be played at once. This function will return {@literal false} if another sound was started
|
* 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);
|
world.sendBlockUpdated(tile.getBlockPos(), tile.getBlockState(), tile.getBlockState(), Block.UPDATE_ALL);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class ErrorMessage {
|
private static final class ErrorMessage {
|
||||||
@Nullable
|
@Nullable
|
||||||
String message;
|
String message;
|
||||||
}
|
}
|
||||||
|
@@ -4,32 +4,30 @@
|
|||||||
|
|
||||||
package dan200.computercraft.shared.turtle.recipes;
|
package dan200.computercraft.shared.turtle.recipes;
|
||||||
|
|
||||||
import com.google.gson.JsonArray;
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import com.google.gson.JsonParseException;
|
|
||||||
import dan200.computercraft.api.turtle.TurtleSide;
|
import dan200.computercraft.api.turtle.TurtleSide;
|
||||||
import dan200.computercraft.shared.ModRegistry;
|
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 dan200.computercraft.shared.turtle.items.TurtleItem;
|
||||||
import net.minecraft.core.NonNullList;
|
|
||||||
import net.minecraft.core.RegistryAccess;
|
import net.minecraft.core.RegistryAccess;
|
||||||
import net.minecraft.network.FriendlyByteBuf;
|
import net.minecraft.network.FriendlyByteBuf;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
import net.minecraft.util.GsonHelper;
|
import net.minecraft.util.GsonHelper;
|
||||||
import net.minecraft.world.inventory.CraftingContainer;
|
import net.minecraft.world.inventory.CraftingContainer;
|
||||||
import net.minecraft.world.item.ItemStack;
|
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.
|
* 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 ResourceLocation overlay;
|
||||||
private final ItemStack result;
|
|
||||||
|
|
||||||
public TurtleOverlayRecipe(ResourceLocation id, String group, CraftingBookCategory category, ItemStack result, NonNullList<Ingredient> ingredients, ResourceLocation overlay) {
|
public TurtleOverlayRecipe(ResourceLocation id, ShapelessRecipeSpec spec, ResourceLocation overlay) {
|
||||||
super(id, group, category, result, ingredients);
|
super(id, spec);
|
||||||
this.overlay = overlay;
|
this.overlay = overlay;
|
||||||
this.result = result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ItemStack make(ItemStack stack, ResourceLocation overlay) {
|
private static ItemStack make(ItemStack stack, ResourceLocation overlay) {
|
||||||
@@ -56,63 +54,29 @@ public class TurtleOverlayRecipe extends ShapelessRecipe {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RecipeSerializer<?> getSerializer() {
|
public RecipeSerializer<TurtleOverlayRecipe> getSerializer() {
|
||||||
return ModRegistry.RecipeSerializers.TURTLE_OVERLAY.get();
|
return ModRegistry.RecipeSerializers.TURTLE_OVERLAY.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Serializer implements RecipeSerializer<TurtleOverlayRecipe> {
|
public static class Serializer implements RecipeSerializer<TurtleOverlayRecipe> {
|
||||||
@Override
|
@Override
|
||||||
public TurtleOverlayRecipe fromJson(ResourceLocation id, JsonObject json) {
|
public TurtleOverlayRecipe fromJson(ResourceLocation id, JsonObject json) {
|
||||||
var group = GsonHelper.getAsString(json, "group", "");
|
var recipe = ShapelessRecipeSpec.fromJson(json);
|
||||||
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 overlay = new ResourceLocation(GsonHelper.getAsString(json, "overlay"));
|
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
|
return new TurtleOverlayRecipe(id, recipe, overlay);
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TurtleOverlayRecipe fromNetwork(ResourceLocation id, FriendlyByteBuf buffer) {
|
public TurtleOverlayRecipe fromNetwork(ResourceLocation id, FriendlyByteBuf buffer) {
|
||||||
var group = buffer.readUtf();
|
var recipe = ShapelessRecipeSpec.fromNetwork(buffer);
|
||||||
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 overlay = buffer.readResourceLocation();
|
var overlay = buffer.readResourceLocation();
|
||||||
|
return new TurtleOverlayRecipe(id, recipe, overlay);
|
||||||
return new TurtleOverlayRecipe(id, group, category, result, items, overlay);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void toNetwork(FriendlyByteBuf buffer, TurtleOverlayRecipe recipe) {
|
public void toNetwork(FriendlyByteBuf buffer, TurtleOverlayRecipe recipe) {
|
||||||
buffer.writeUtf(recipe.getGroup());
|
recipe.toSpec().toNetwork(buffer);
|
||||||
buffer.writeEnum(recipe.category());
|
|
||||||
buffer.writeVarInt(recipe.getIngredients().size());
|
|
||||||
|
|
||||||
for (var ingredient : recipe.getIngredients()) ingredient.toNetwork(buffer);
|
|
||||||
buffer.writeItem(recipe.result);
|
|
||||||
buffer.writeResourceLocation(recipe.overlay);
|
buffer.writeResourceLocation(recipe.overlay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,26 +4,33 @@
|
|||||||
|
|
||||||
package dan200.computercraft.shared.turtle.recipes;
|
package dan200.computercraft.shared.turtle.recipes;
|
||||||
|
|
||||||
|
import com.mojang.serialization.DataResult;
|
||||||
import dan200.computercraft.shared.ModRegistry;
|
import dan200.computercraft.shared.ModRegistry;
|
||||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
|
||||||
import dan200.computercraft.shared.computer.items.IComputerItem;
|
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 dan200.computercraft.shared.turtle.items.TurtleItem;
|
||||||
import net.minecraft.core.NonNullList;
|
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
import net.minecraft.world.item.ItemStack;
|
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.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) {
|
* The recipe which crafts a turtle from an existing computer item.
|
||||||
super(identifier, group, category, width, height, ingredients, result, family);
|
*/
|
||||||
|
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 static DataResult<TurtleRecipe> of(ResourceLocation id, ShapedRecipeSpec recipe) {
|
||||||
public RecipeSerializer<?> getSerializer() {
|
if (!(recipe.result().getItem() instanceof TurtleItem turtle)) {
|
||||||
return ModRegistry.RecipeSerializers.TURTLE.get();
|
return DataResult.error(() -> recipe.result().getItem() + " is not a turtle item");
|
||||||
|
}
|
||||||
|
|
||||||
|
return DataResult.success(new TurtleRecipe(id, recipe, turtle));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -31,13 +38,11 @@ public final class TurtleRecipe extends ComputerFamilyRecipe {
|
|||||||
var computerID = item.getComputerID(stack);
|
var computerID = item.getComputerID(stack);
|
||||||
var label = item.getLabel(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
|
||||||
@Override
|
public RecipeSerializer<TurtleRecipe> getSerializer() {
|
||||||
protected TurtleRecipe create(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family) {
|
return ModRegistry.RecipeSerializers.TURTLE.get();
|
||||||
return new TurtleRecipe(identifier, group, category, width, height, ingredients, result, family);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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.synopsis": "Různé příkazy pro ovládání počítačů.",
|
||||||
"commands.computercraft.tp.action": "Teleportovat se k počítači",
|
"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.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.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.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č",
|
"commands.computercraft.track.dump.computer": "Počítač",
|
||||||
|
@@ -46,8 +46,6 @@
|
|||||||
"commands.computercraft.synopsis": "Verschiedene Befehle um Computer zu kontrollieren.",
|
"commands.computercraft.synopsis": "Verschiedene Befehle um Computer zu kontrollieren.",
|
||||||
"commands.computercraft.tp.action": "Teleportiert dich zum Computer",
|
"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.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.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.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",
|
"commands.computercraft.track.dump.computer": "Computer",
|
||||||
|
@@ -47,8 +47,6 @@
|
|||||||
"commands.computercraft.synopsis": "Commandes diverses pour contrôler les ordinateurs.",
|
"commands.computercraft.synopsis": "Commandes diverses pour contrôler les ordinateurs.",
|
||||||
"commands.computercraft.tp.action": "Se téléporter vers cet ordinateur",
|
"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.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.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.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",
|
"commands.computercraft.track.dump.computer": "Ordinateur",
|
||||||
|
@@ -47,8 +47,6 @@
|
|||||||
"commands.computercraft.synopsis": "Vari comandi per controllare i computer.",
|
"commands.computercraft.synopsis": "Vari comandi per controllare i computer.",
|
||||||
"commands.computercraft.tp.action": "Teletrasporta a questo 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.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.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.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",
|
"commands.computercraft.track.dump.computer": "Computer",
|
||||||
|
@@ -47,8 +47,6 @@
|
|||||||
"commands.computercraft.synopsis": "コンピュータを制御するためのさまざまなコマンド。",
|
"commands.computercraft.synopsis": "コンピュータを制御するためのさまざまなコマンド。",
|
||||||
"commands.computercraft.tp.action": "このコンピューターへテレポートします",
|
"commands.computercraft.tp.action": "このコンピューターへテレポートします",
|
||||||
"commands.computercraft.tp.desc": "コンピュータの場所にテレポート.コンピュータのインスタンスID(例えば 123)またはコンピュータID(例えば #123)を指定することができます。",
|
"commands.computercraft.tp.desc": "コンピュータの場所にテレポート.コンピュータのインスタンスID(例えば 123)またはコンピュータID(例えば #123)を指定することができます。",
|
||||||
"commands.computercraft.tp.not_player": "非プレイヤー用のターミナルを開くことができません",
|
|
||||||
"commands.computercraft.tp.not_there": "世界でコンピュータを見つけることができませんでした",
|
|
||||||
"commands.computercraft.tp.synopsis": "特定のコンピュータにテレポート。",
|
"commands.computercraft.tp.synopsis": "特定のコンピュータにテレポート。",
|
||||||
"commands.computercraft.track.desc": "コンピュータの実行時間を追跡するだけでなく、イベントを確認することができます。 これは /forge と同様の方法で情報を提示し、遅れを診断するのに役立ちます。",
|
"commands.computercraft.track.desc": "コンピュータの実行時間を追跡するだけでなく、イベントを確認することができます。 これは /forge と同様の方法で情報を提示し、遅れを診断するのに役立ちます。",
|
||||||
"commands.computercraft.track.dump.computer": "コンピューター",
|
"commands.computercraft.track.dump.computer": "コンピューター",
|
||||||
|
@@ -47,8 +47,6 @@
|
|||||||
"commands.computercraft.synopsis": "컴퓨터를 제어하기 위한 다양한 명령어",
|
"commands.computercraft.synopsis": "컴퓨터를 제어하기 위한 다양한 명령어",
|
||||||
"commands.computercraft.tp.action": "이 컴퓨터로 순간이동하기",
|
"commands.computercraft.tp.action": "이 컴퓨터로 순간이동하기",
|
||||||
"commands.computercraft.tp.desc": "컴퓨터의 위치로 순간이동합니다. 컴퓨터의 인스턴스 ID(예: 123) 또는 컴퓨터 ID(예: #123)를 지정할 수 있습니다.",
|
"commands.computercraft.tp.desc": "컴퓨터의 위치로 순간이동합니다. 컴퓨터의 인스턴스 ID(예: 123) 또는 컴퓨터 ID(예: #123)를 지정할 수 있습니다.",
|
||||||
"commands.computercraft.tp.not_player": "비플레이어용 터미널을 열 수 없습니다.",
|
|
||||||
"commands.computercraft.tp.not_there": "월드에서 컴퓨터를 위치시킬 수 없습니다.",
|
|
||||||
"commands.computercraft.tp.synopsis": "특정 컴퓨터로 순간이동하기",
|
"commands.computercraft.tp.synopsis": "특정 컴퓨터로 순간이동하기",
|
||||||
"commands.computercraft.track.desc": "컴퓨터가 실행되는 기간과 처리되는 이벤트 수를 추적합니다. 이는 /forge 트랙과 유사한 방법으로 정보를 제공하며 지연 로그에 유용할 수 있습니다.",
|
"commands.computercraft.track.desc": "컴퓨터가 실행되는 기간과 처리되는 이벤트 수를 추적합니다. 이는 /forge 트랙과 유사한 방법으로 정보를 제공하며 지연 로그에 유용할 수 있습니다.",
|
||||||
"commands.computercraft.track.dump.computer": "컴퓨터",
|
"commands.computercraft.track.dump.computer": "컴퓨터",
|
||||||
|
@@ -47,8 +47,6 @@
|
|||||||
"commands.computercraft.synopsis": "Forskjellige kommandoer for å kontrollere datamaskiner.",
|
"commands.computercraft.synopsis": "Forskjellige kommandoer for å kontrollere datamaskiner.",
|
||||||
"commands.computercraft.tp.action": "Teleporter til denne datamaskinen",
|
"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.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.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.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",
|
"commands.computercraft.track.dump.computer": "Datamaskin",
|
||||||
|
@@ -47,8 +47,6 @@
|
|||||||
"commands.computercraft.synopsis": "Verschillende commando's voor het beheren van computers.",
|
"commands.computercraft.synopsis": "Verschillende commando's voor het beheren van computers.",
|
||||||
"commands.computercraft.tp.action": "Teleporteer naar deze computer",
|
"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.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.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.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",
|
"commands.computercraft.track.dump.computer": "Computer",
|
||||||
|
@@ -31,7 +31,6 @@
|
|||||||
"commands.computercraft.synopsis": "Różne komendy do kontrolowania komputerami.",
|
"commands.computercraft.synopsis": "Różne komendy do kontrolowania komputerami.",
|
||||||
"commands.computercraft.tp.action": "Przeteleportuj się do podanego komputera",
|
"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.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.tp.synopsis": "Przeteleportuj się do podanego komputera.",
|
||||||
"commands.computercraft.track.dump.computer": "Komputer",
|
"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\").",
|
"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.synopsis": "Различные команды для управления компьютерами.",
|
||||||
"commands.computercraft.tp.action": "Телепортироваться к этому компьютеру",
|
"commands.computercraft.tp.action": "Телепортироваться к этому компьютеру",
|
||||||
"commands.computercraft.tp.desc": "Телепортироваться к местоположению компьютера. Ты можешь указать либо идентификатор экземпляра компьютера (например 123) либо идентификатор компьютера (например #123).",
|
"commands.computercraft.tp.desc": "Телепортироваться к местоположению компьютера. Ты можешь указать либо идентификатор экземпляра компьютера (например 123) либо идентификатор компьютера (например #123).",
|
||||||
"commands.computercraft.tp.not_player": "Нельзя открыть терминал для не-игрока",
|
|
||||||
"commands.computercraft.tp.not_there": "Нельзя определить в мире местоположение компьютер",
|
|
||||||
"commands.computercraft.tp.synopsis": "Телепортироваться к конкретному компьютеру.",
|
"commands.computercraft.tp.synopsis": "Телепортироваться к конкретному компьютеру.",
|
||||||
"commands.computercraft.track.desc": "Отслеживает, как долго компьютеры исполняют, а также то, как много они обрабатывают события. Эта информация представляется аналогично к /forge track и может быть полезной для диагностики лага.",
|
"commands.computercraft.track.desc": "Отслеживает, как долго компьютеры исполняют, а также то, как много они обрабатывают события. Эта информация представляется аналогично к /forge track и может быть полезной для диагностики лага.",
|
||||||
"commands.computercraft.track.dump.computer": "Компьютер",
|
"commands.computercraft.track.dump.computer": "Компьютер",
|
||||||
|
@@ -47,8 +47,6 @@
|
|||||||
"commands.computercraft.synopsis": "Olika kommandon för att kontrollera datorer.",
|
"commands.computercraft.synopsis": "Olika kommandon för att kontrollera datorer.",
|
||||||
"commands.computercraft.tp.action": "Teleportera till den här datorn",
|
"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.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.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.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",
|
"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.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.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.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.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.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",
|
"commands.computercraft.track.dump.computer": "ilo sona",
|
||||||
|
@@ -47,8 +47,6 @@
|
|||||||
"commands.computercraft.synopsis": "Різні команди для керування комп'ютерами.",
|
"commands.computercraft.synopsis": "Різні команди для керування комп'ютерами.",
|
||||||
"commands.computercraft.tp.action": "Телепортувати до цього комп'ютера",
|
"commands.computercraft.tp.action": "Телепортувати до цього комп'ютера",
|
||||||
"commands.computercraft.tp.desc": "Телепортувати до розташування комп'ютера. Ви можете вказати або ідентифікатор екземпляра комп'ютера (наприклад, 123) або ідентифікатор комп'ютера (наприклад, #123).",
|
"commands.computercraft.tp.desc": "Телепортувати до розташування комп'ютера. Ви можете вказати або ідентифікатор екземпляра комп'ютера (наприклад, 123) або ідентифікатор комп'ютера (наприклад, #123).",
|
||||||
"commands.computercraft.tp.not_player": "Не можна відкрити термінал для не-гравця",
|
|
||||||
"commands.computercraft.tp.not_there": "Не можна визначити у світі розташування комп'ютер",
|
|
||||||
"commands.computercraft.tp.synopsis": "Телепортувати до конкретного комп'ютера.",
|
"commands.computercraft.tp.synopsis": "Телепортувати до конкретного комп'ютера.",
|
||||||
"commands.computercraft.track.desc": "Відстежує, як довго комп'ютери виконують, а також те, як багато вони обробляють події. Ця інформація представляється аналогічно /forge track і може бути корисною для діагностики лага.",
|
"commands.computercraft.track.desc": "Відстежує, як довго комп'ютери виконують, а також те, як багато вони обробляють події. Ця інформація представляється аналогічно /forge track і може бути корисною для діагностики лага.",
|
||||||
"commands.computercraft.track.dump.computer": "Комп'ютер",
|
"commands.computercraft.track.dump.computer": "Комп'ютер",
|
||||||
|
@@ -45,8 +45,6 @@
|
|||||||
"commands.computercraft.synopsis": "各种控制计算机的命令.",
|
"commands.computercraft.synopsis": "各种控制计算机的命令.",
|
||||||
"commands.computercraft.tp.action": "传送到这台电脑",
|
"commands.computercraft.tp.action": "传送到这台电脑",
|
||||||
"commands.computercraft.tp.desc": "传送到计算机的位置. 你可以指定计算机的实例id (例如. 123)或计算机id (例如. #123).",
|
"commands.computercraft.tp.desc": "传送到计算机的位置. 你可以指定计算机的实例id (例如. 123)或计算机id (例如. #123).",
|
||||||
"commands.computercraft.tp.not_player": "无法为非玩家打开终端",
|
|
||||||
"commands.computercraft.tp.not_there": "无法在世界上定位电脑",
|
|
||||||
"commands.computercraft.tp.synopsis": "传送到特定的计算机.",
|
"commands.computercraft.tp.synopsis": "传送到特定的计算机.",
|
||||||
"commands.computercraft.track.desc": "跟踪计算机执行的时间以及它们处理的事件数. 这以/forge track类似的方式呈现信息,可用于诊断滞后.",
|
"commands.computercraft.track.desc": "跟踪计算机执行的时间以及它们处理的事件数. 这以/forge track类似的方式呈现信息,可用于诊断滞后.",
|
||||||
"commands.computercraft.track.dump.computer": "计算机",
|
"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
|
@Override
|
||||||
public String getType() {
|
public String getType() {
|
||||||
return "test";
|
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.api.turtle.TurtleToolDurability;
|
||||||
import dan200.computercraft.test.core.StructuralEquality;
|
import dan200.computercraft.test.core.StructuralEquality;
|
||||||
import dan200.computercraft.test.shared.MinecraftArbitraries;
|
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 dan200.computercraft.test.shared.WithMinecraft;
|
||||||
import io.netty.buffer.Unpooled;
|
|
||||||
import net.jqwik.api.*;
|
import net.jqwik.api.*;
|
||||||
import net.minecraft.core.registries.Registries;
|
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.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
|
|
||||||
@WithMinecraft
|
@WithMinecraft
|
||||||
class TurtleToolSerialiserTest {
|
class TurtleToolSerialiserTest {
|
||||||
@@ -32,11 +29,9 @@ class TurtleToolSerialiserTest {
|
|||||||
*/
|
*/
|
||||||
@Property
|
@Property
|
||||||
public void testRoundTrip(@ForAll("tool") TurtleTool tool) {
|
public void testRoundTrip(@ForAll("tool") TurtleTool tool) {
|
||||||
var buffer = new FriendlyByteBuf(Unpooled.directBuffer());
|
var converted = NetworkSupport.roundTripSerialiser(
|
||||||
TurtleToolSerialiser.INSTANCE.toNetwork(buffer, tool);
|
tool.getUpgradeID(), tool, TurtleToolSerialiser.INSTANCE::toNetwork, TurtleToolSerialiser.INSTANCE::fromNetwork
|
||||||
|
);
|
||||||
var converted = TurtleToolSerialiser.INSTANCE.fromNetwork(tool.getUpgradeID(), buffer);
|
|
||||||
assertEquals(buffer.readableBytes(), 0, "Whole packet was read");
|
|
||||||
|
|
||||||
if (!equality.equals(tool, converted)) {
|
if (!equality.equals(tool, converted)) {
|
||||||
System.out.println("Break");
|
System.out.println("Break");
|
||||||
@@ -58,22 +53,10 @@ class TurtleToolSerialiserTest {
|
|||||||
).as(TurtleTool::new);
|
).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(
|
private static final StructuralEquality<TurtleTool> equality = StructuralEquality.all(
|
||||||
StructuralEquality.at("id", ITurtleUpgrade::getUpgradeID),
|
StructuralEquality.at("id", ITurtleUpgrade::getUpgradeID),
|
||||||
StructuralEquality.at("craftingItem", ITurtleUpgrade::getCraftingItem, stackEquality),
|
StructuralEquality.at("craftingItem", ITurtleUpgrade::getCraftingItem, MinecraftEqualities.itemStack),
|
||||||
StructuralEquality.at("tool", x -> x.item, stackEquality),
|
StructuralEquality.at("tool", x -> x.item, MinecraftEqualities.itemStack),
|
||||||
StructuralEquality.at("damageMulitiplier", x -> x.damageMulitiplier),
|
StructuralEquality.at("damageMulitiplier", x -> x.damageMulitiplier),
|
||||||
StructuralEquality.at("allowEnchantments", x -> x.allowEnchantments),
|
StructuralEquality.at("allowEnchantments", x -> x.allowEnchantments),
|
||||||
StructuralEquality.at("consumeDurability", x -> x.consumeDurability),
|
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.Item;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.minecraft.world.item.Items;
|
import net.minecraft.world.item.Items;
|
||||||
|
import net.minecraft.world.item.crafting.Ingredient;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -44,6 +45,10 @@ public final class MinecraftArbitraries {
|
|||||||
return Arbitraries.oneOf(List.of(Arbitraries.just(ItemStack.EMPTY), nonEmptyItemStack()));
|
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() {
|
public static Arbitrary<BlockPos> blockPos() {
|
||||||
// BlockPos has a maximum range that can be sent over the network - use those.
|
// 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);
|
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
|
package dan200.computercraft.gametest
|
||||||
|
|
||||||
|
import com.mojang.authlib.GameProfile
|
||||||
import dan200.computercraft.gametest.api.Structures
|
import dan200.computercraft.gametest.api.Structures
|
||||||
import dan200.computercraft.gametest.api.sequence
|
import dan200.computercraft.gametest.api.sequence
|
||||||
import dan200.computercraft.shared.ModRegistry
|
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.GameTestAssertException
|
||||||
import net.minecraft.gametest.framework.GameTestHelper
|
import net.minecraft.gametest.framework.GameTestHelper
|
||||||
import net.minecraft.nbt.CompoundTag
|
import net.minecraft.nbt.CompoundTag
|
||||||
|
import net.minecraft.nbt.NbtUtils
|
||||||
import net.minecraft.world.entity.player.Player
|
import net.minecraft.world.entity.player.Player
|
||||||
import net.minecraft.world.inventory.AbstractContainerMenu
|
import net.minecraft.world.inventory.AbstractContainerMenu
|
||||||
import net.minecraft.world.inventory.MenuType
|
import net.minecraft.world.inventory.MenuType
|
||||||
@@ -41,11 +43,10 @@ class Recipe_Test {
|
|||||||
|
|
||||||
val result = recipe.get().assemble(container, context.level.registryAccess())
|
val result = recipe.get().assemble(container, context.level.registryAccess())
|
||||||
|
|
||||||
val owner = CompoundTag()
|
val profile = GameProfile(UUID.fromString("f3c8d69b-0776-4512-8434-d1b2165909eb"), "dan200")
|
||||||
owner.putString("Name", "dan200")
|
|
||||||
owner.putString("Id", "f3c8d69b-0776-4512-8434-d1b2165909eb")
|
|
||||||
val tag = CompoundTag()
|
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")
|
assertEquals(tag, result.tag, "Expected NBT tags to be the same")
|
||||||
}
|
}
|
||||||
|
@@ -18,9 +18,7 @@ val docApi by configurations.registering {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnlyApi(libs.jsr305)
|
compileOnlyApi(libs.bundles.annotations)
|
||||||
compileOnlyApi(libs.checkerFramework)
|
|
||||||
compileOnlyApi(libs.jetbrainsAnnotations)
|
|
||||||
|
|
||||||
"docApi"(project(":common-api"))
|
"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.
|
* 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 isDirectory Whether this filesystem entry is a directory.
|
||||||
* @param size The size of the file.
|
* @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);
|
private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
public FileTime lastModifiedTime() {
|
* Create a new {@link FileAttributes} instance with the {@linkplain #creationTime() creation time} and
|
||||||
return EPOCH;
|
* {@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
|
@Override
|
||||||
public FileTime lastAccessTime() {
|
public FileTime lastAccessTime() {
|
||||||
return EPOCH;
|
return lastModifiedTime();
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FileTime creationTime() {
|
|
||||||
return EPOCH;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@@ -17,7 +17,6 @@ import dan200.computercraft.core.filesystem.FileSystemException;
|
|||||||
import dan200.computercraft.core.metrics.Metrics;
|
import dan200.computercraft.core.metrics.Metrics;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.nio.file.attribute.FileTime;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
@@ -488,9 +487,9 @@ public class FSAPI implements ILuaAPI {
|
|||||||
try (var ignored = environment.time(Metrics.FS_OPS)) {
|
try (var ignored = environment.time(Metrics.FS_OPS)) {
|
||||||
var attributes = getFileSystem().getAttributes(path);
|
var attributes = getFileSystem().getAttributes(path);
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
result.put("modification", getFileTime(attributes.lastModifiedTime()));
|
result.put("modification", attributes.lastModifiedTime().toMillis());
|
||||||
result.put("modified", getFileTime(attributes.lastModifiedTime()));
|
result.put("modified", attributes.lastModifiedTime().toMillis());
|
||||||
result.put("created", getFileTime(attributes.creationTime()));
|
result.put("created", attributes.creationTime().toMillis());
|
||||||
result.put("size", attributes.isDirectory() ? 0 : attributes.size());
|
result.put("size", attributes.isDirectory() ? 0 : attributes.size());
|
||||||
result.put("isDir", attributes.isDirectory());
|
result.put("isDir", attributes.isDirectory());
|
||||||
result.put("isReadOnly", getFileSystem().isReadOnly(path));
|
result.put("isReadOnly", getFileSystem().isReadOnly(path));
|
||||||
@@ -499,8 +498,4 @@ public class FSAPI implements ILuaAPI {
|
|||||||
throw new LuaException(e.getMessage());
|
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.*;
|
||||||
import dan200.computercraft.core.apis.http.request.HttpRequest;
|
import dan200.computercraft.core.apis.http.request.HttpRequest;
|
||||||
import dan200.computercraft.core.apis.http.websocket.Websocket;
|
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.DefaultHttpHeaders;
|
||||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||||
import io.netty.handler.codec.http.HttpHeaders;
|
import io.netty.handler.codec.http.HttpHeaders;
|
||||||
@@ -165,7 +166,7 @@ public class HTTPAPI implements ILuaAPI {
|
|||||||
var timeout = getTimeout(timeoutArg);
|
var timeout = getTimeout(timeoutArg);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var uri = Websocket.checkUri(address);
|
var uri = WebsocketClient.parseUri(address);
|
||||||
if (!new Websocket(websockets, apiEnvironment, uri, address, headers, timeout).queue(Websocket::connect)) {
|
if (!new Websocket(websockets, apiEnvironment, uri, address, headers, timeout).queue(Websocket::connect)) {
|
||||||
throw new LuaException("Too many websockets already open");
|
throw new LuaException("Too many websockets already open");
|
||||||
}
|
}
|
||||||
|
@@ -49,7 +49,7 @@ import java.util.List;
|
|||||||
* end
|
* end
|
||||||
* }</pre>
|
* }</pre>
|
||||||
* <p>
|
* <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."
|
* the Minecraft wiki."
|
||||||
* @cc.module redstone
|
* @cc.module redstone
|
||||||
*/
|
*/
|
||||||
|
@@ -77,12 +77,10 @@ public class BinaryReadableHandle extends HandleGeneric {
|
|||||||
var buffer = ByteBuffer.allocate(BUFFER_SIZE);
|
var buffer = ByteBuffer.allocate(BUFFER_SIZE);
|
||||||
var read = channel.read(buffer);
|
var read = channel.read(buffer);
|
||||||
if (read < 0) return null;
|
if (read < 0) return null;
|
||||||
|
buffer.flip();
|
||||||
|
|
||||||
// If we failed to read "enough" here, let's just abort
|
// If we failed to read "enough" here, let's just abort
|
||||||
if (read >= count || read < BUFFER_SIZE) {
|
if (read >= count || read < BUFFER_SIZE) return new Object[]{ buffer };
|
||||||
buffer.flip();
|
|
||||||
return new Object[]{ buffer };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build up an array of ByteBuffers. Hopefully this means we can perform less allocation
|
// Build up an array of ByteBuffers. Hopefully this means we can perform less allocation
|
||||||
// than doubling up the buffer each time.
|
// than doubling up the buffer each time.
|
||||||
@@ -90,11 +88,13 @@ public class BinaryReadableHandle extends HandleGeneric {
|
|||||||
List<ByteBuffer> parts = new ArrayList<>(4);
|
List<ByteBuffer> parts = new ArrayList<>(4);
|
||||||
parts.add(buffer);
|
parts.add(buffer);
|
||||||
while (read >= BUFFER_SIZE && totalRead < count) {
|
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);
|
read = channel.read(buffer);
|
||||||
if (read < 0) break;
|
if (read < 0) break;
|
||||||
|
buffer.flip();
|
||||||
|
|
||||||
totalRead += read;
|
totalRead += read;
|
||||||
|
assert read == buffer.remaining();
|
||||||
parts.add(buffer);
|
parts.add(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,9 +102,11 @@ public class BinaryReadableHandle extends HandleGeneric {
|
|||||||
var bytes = new byte[totalRead];
|
var bytes = new byte[totalRead];
|
||||||
var pos = 0;
|
var pos = 0;
|
||||||
for (var part : parts) {
|
for (var part : parts) {
|
||||||
System.arraycopy(part.array(), 0, bytes, pos, part.position());
|
int length = part.remaining();
|
||||||
pos += part.position();
|
part.get(bytes, pos, length);
|
||||||
|
pos += length;
|
||||||
}
|
}
|
||||||
|
assert pos == totalRead;
|
||||||
return new Object[]{ bytes };
|
return new Object[]{ bytes };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@@ -134,7 +134,7 @@ public final class NetworkUtils {
|
|||||||
*/
|
*/
|
||||||
public static Options getOptions(String host, InetSocketAddress address) throws HTTPRequestException {
|
public static Options getOptions(String host, InetSocketAddress address) throws HTTPRequestException {
|
||||||
var options = AddressRule.apply(CoreConfig.httpRules, host, address);
|
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;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ public final class NetworkUtils {
|
|||||||
* @throws HTTPRequestException If a proxy is required but not configured correctly.
|
* @throws HTTPRequestException If a proxy is required but not configured correctly.
|
||||||
*/
|
*/
|
||||||
public static @Nullable Consumer<SocketChannel> getProxyHandler(Options options, int timeout) throws HTTPRequestException {
|
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 type = CoreConfig.httpProxyType;
|
||||||
var host = CoreConfig.httpProxyHost;
|
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 record Options(Action action, long maxUpload, long maxDownload, int websocketMessage, boolean useProxy) {
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -34,7 +34,7 @@ public final class PartialOptions {
|
|||||||
this.useProxy = useProxy;
|
this.useProxy = useProxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
Options toOptions() {
|
public Options toOptions() {
|
||||||
if (options != null) return options;
|
if (options != null) return options;
|
||||||
|
|
||||||
return options = new Options(
|
return options = new Options(
|
||||||
|
@@ -130,7 +130,7 @@ public class HttpRequest extends Resource<HttpRequest> {
|
|||||||
if (isClosed()) return;
|
if (isClosed()) return;
|
||||||
|
|
||||||
var requestBody = getHeaderSize(headers) + postBuffer.capacity();
|
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");
|
failure("Request body is too large");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@@ -136,7 +136,7 @@ public final class HttpRequestHandler extends SimpleChannelInboundHandler<HttpOb
|
|||||||
var partial = content.content();
|
var partial = content.content();
|
||||||
if (partial.isReadable()) {
|
if (partial.isReadable()) {
|
||||||
// If we've read more than we're allowed to handle, abort as soon as possible.
|
// 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;
|
closed = true;
|
||||||
ctx.close();
|
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
|
* A version of {@link WebSocketClientHandshaker13} which doesn't add the {@link HttpHeaderNames#ORIGIN} header to the
|
||||||
* original HTTP request.
|
* original HTTP request.
|
||||||
*/
|
*/
|
||||||
public class NoOriginWebSocketHandshaker extends WebSocketClientHandshaker13 {
|
class NoOriginWebSocketHandshaker extends WebSocketClientHandshaker13 {
|
||||||
public NoOriginWebSocketHandshaker(URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) {
|
NoOriginWebSocketHandshaker(URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) {
|
||||||
super(webSocketURL, version, subprotocol, allowExtensions, customHeaders, 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.Resource;
|
||||||
import dan200.computercraft.core.apis.http.ResourceGroup;
|
import dan200.computercraft.core.apis.http.ResourceGroup;
|
||||||
import dan200.computercraft.core.apis.http.options.Options;
|
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.bootstrap.Bootstrap;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
import io.netty.channel.Channel;
|
import io.netty.channel.Channel;
|
||||||
import io.netty.channel.ChannelFuture;
|
import io.netty.channel.ChannelFuture;
|
||||||
import io.netty.channel.ChannelInitializer;
|
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.HttpHeaderNames;
|
||||||
import io.netty.handler.codec.http.HttpHeaders;
|
import io.netty.handler.codec.http.HttpHeaders;
|
||||||
import io.netty.handler.codec.http.HttpObjectAggregator;
|
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.WebSocketClientProtocolHandler;
|
||||||
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
|
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides functionality to verify and connect to a remote websocket.
|
* 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);
|
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;
|
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 Future<?> executorFuture;
|
||||||
private @Nullable ChannelFuture connectFuture;
|
private @Nullable ChannelFuture channelFuture;
|
||||||
private @Nullable WeakReference<WebsocketHandle> websocketHandle;
|
|
||||||
|
|
||||||
private final IAPIEnvironment environment;
|
private final IAPIEnvironment environment;
|
||||||
private final URI uri;
|
private final URI uri;
|
||||||
@@ -70,38 +66,6 @@ public class Websocket extends Resource<Websocket> {
|
|||||||
this.timeout = timeout;
|
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() {
|
public void connect() {
|
||||||
if (isClosed()) return;
|
if (isClosed()) return;
|
||||||
executorFuture = NetworkUtils.EXECUTOR.submit(this::doConnect);
|
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.
|
// getAddress may have a slight delay, so let's perform another cancellation check.
|
||||||
if (isClosed()) return;
|
if (isClosed()) return;
|
||||||
|
|
||||||
connectFuture = new Bootstrap()
|
channelFuture = new Bootstrap()
|
||||||
.group(NetworkUtils.LOOP_GROUP)
|
.group(NetworkUtils.LOOP_GROUP)
|
||||||
.channel(NioSocketChannel.class)
|
.channel(NioSocketChannel.class)
|
||||||
.handler(new ChannelInitializer<SocketChannel>() {
|
.handler(new ChannelInitializer<SocketChannel>() {
|
||||||
@@ -133,7 +97,7 @@ public class Websocket extends Resource<Websocket> {
|
|||||||
var subprotocol = headers.get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL);
|
var subprotocol = headers.get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL);
|
||||||
var handshaker = new NoOriginWebSocketHandshaker(
|
var handshaker = new NoOriginWebSocketHandshaker(
|
||||||
uri, WebSocketVersion.V13, subprotocol, true, headers,
|
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();
|
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;
|
if (isClosed()) return;
|
||||||
|
|
||||||
var handle = new WebsocketHandle(this, options, channel);
|
var handle = new WebsocketHandle(environment, address, this, options);
|
||||||
environment().queueEvent(SUCCESS_EVENT, address, handle);
|
environment().queueEvent(SUCCESS_EVENT, address, handle);
|
||||||
websocketHandle = createOwnerReference(handle);
|
createOwnerReference(handle);
|
||||||
|
|
||||||
checkClosed();
|
checkClosed();
|
||||||
}
|
}
|
||||||
@@ -189,19 +153,35 @@ public class Websocket extends Resource<Websocket> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
|
|
||||||
executorFuture = closeFuture(executorFuture);
|
executorFuture = closeFuture(executorFuture);
|
||||||
connectFuture = closeChannel(connectFuture);
|
channelFuture = closeChannel(channelFuture);
|
||||||
|
|
||||||
var websocketHandleRef = websocketHandle;
|
|
||||||
var websocketHandle = websocketHandleRef == null ? null : websocketHandleRef.get();
|
|
||||||
IoUtil.closeQuietly(websocketHandle);
|
|
||||||
this.websocketHandle = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IAPIEnvironment environment() {
|
IAPIEnvironment environment() {
|
||||||
return environment;
|
return environment;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String address() {
|
String address() {
|
||||||
return 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