mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-10-18 07:27:39 +00:00
Compare commits
16 Commits
v1.20.1-1.
...
v1.20.1-1.
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e7ab05d064 | ||
![]() |
6ec34b42e5 | ||
![]() |
ab785a0906 | ||
![]() |
4541decd40 | ||
![]() |
747a5a53b4 | ||
![]() |
c0643fadca | ||
![]() |
0a31de43c2 | ||
![]() |
96b6947ef2 | ||
![]() |
e7a1065bfc | ||
![]() |
663eecff0c | ||
![]() |
e6125bcf60 | ||
![]() |
0d6c6e7ae7 | ||
![]() |
ae71eb3cae | ||
![]() |
3188197447 | ||
![]() |
6c8b391dab | ||
![]() |
b1248e4901 |
@@ -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"
|
||||||
|
@@ -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
|
||||||
|
@@ -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.1
|
modVersion=1.108.2
|
||||||
|
|
||||||
# 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
|
||||||
|
@@ -30,7 +30,7 @@ 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.7"
|
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"
|
||||||
@@ -60,7 +60,7 @@ fabric-loom = "1.3.7"
|
|||||||
forgeGradle = "6.0.8"
|
forgeGradle = "6.0.8"
|
||||||
githubRelease = "2.4.1"
|
githubRelease = "2.4.1"
|
||||||
ideaExt = "1.1.7"
|
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.+"
|
||||||
@@ -69,10 +69,12 @@ spotless = "6.21.0"
|
|||||||
taskTree = "2.1.1"
|
taskTree = "2.1.1"
|
||||||
vanillaGradle = "0.2.1-SNAPSHOT"
|
vanillaGradle = "0.2.1-SNAPSHOT"
|
||||||
vineflower = "1.11.0"
|
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" }
|
||||||
@@ -138,6 +140,14 @@ 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" }
|
||||||
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" }
|
vineflower = { module = "io.github.juuxel:loom-vineflower", version.ref = "vineflower" }
|
||||||
|
|
||||||
@@ -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__)))
|
||||||
|
3957
package-lock.json
generated
3957
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
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": "^10.3.4",
|
"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.0",
|
"rollup": "^4.0.0",
|
||||||
"requirejs": "^2.3.6",
|
"tsx": "^3.12.10",
|
||||||
"rollup": "^3.19.1",
|
|
||||||
"ts-node": "^10.8.0",
|
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
@@ -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.
|
||||||
);
|
);
|
||||||
|
@@ -70,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;
|
||||||
@@ -79,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;
|
||||||
@@ -359,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 {
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
|
@@ -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,70 @@
|
|||||||
|
// 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) {
|
||||||
|
return buffer.readCollection(x -> NonNullList.withSize(x, Ingredient.EMPTY), Ingredient::fromNetwork);
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
@@ -6,40 +6,34 @@ package dan200.computercraft.core.apis.http.websocket;
|
|||||||
|
|
||||||
import com.google.common.base.Objects;
|
import com.google.common.base.Objects;
|
||||||
import dan200.computercraft.api.lua.*;
|
import dan200.computercraft.api.lua.*;
|
||||||
|
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||||
import dan200.computercraft.core.apis.http.options.Options;
|
import dan200.computercraft.core.apis.http.options.Options;
|
||||||
import dan200.computercraft.core.metrics.Metrics;
|
|
||||||
import io.netty.buffer.Unpooled;
|
|
||||||
import io.netty.channel.Channel;
|
|
||||||
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
|
|
||||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import java.io.Closeable;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import static dan200.computercraft.api.lua.LuaValues.checkFinite;
|
import static dan200.computercraft.api.lua.LuaValues.checkFinite;
|
||||||
import static dan200.computercraft.core.apis.IAPIEnvironment.TIMER_EVENT;
|
import static dan200.computercraft.core.apis.IAPIEnvironment.TIMER_EVENT;
|
||||||
import static dan200.computercraft.core.apis.http.websocket.Websocket.CLOSE_EVENT;
|
import static dan200.computercraft.core.apis.http.websocket.WebsocketClient.CLOSE_EVENT;
|
||||||
import static dan200.computercraft.core.apis.http.websocket.Websocket.MESSAGE_EVENT;
|
import static dan200.computercraft.core.apis.http.websocket.WebsocketClient.MESSAGE_EVENT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A websocket, which can be used to send an receive messages with a web server.
|
* A websocket, which can be used to send and receive messages with a web server.
|
||||||
*
|
*
|
||||||
* @cc.module http.Websocket
|
* @cc.module http.Websocket
|
||||||
* @see dan200.computercraft.core.apis.HTTPAPI#websocket On how to open a websocket.
|
* @see dan200.computercraft.core.apis.HTTPAPI#websocket On how to open a websocket.
|
||||||
*/
|
*/
|
||||||
public class WebsocketHandle implements Closeable {
|
public class WebsocketHandle {
|
||||||
private final Websocket websocket;
|
private final IAPIEnvironment environment;
|
||||||
|
private final String address;
|
||||||
|
private final WebsocketClient websocket;
|
||||||
private final Options options;
|
private final Options options;
|
||||||
private boolean closed = false;
|
|
||||||
|
|
||||||
private @Nullable Channel channel;
|
public WebsocketHandle(IAPIEnvironment environment, String address, WebsocketClient websocket, Options options) {
|
||||||
|
this.environment = environment;
|
||||||
public WebsocketHandle(Websocket websocket, Options options, Channel channel) {
|
this.address = address;
|
||||||
this.websocket = websocket;
|
this.websocket = websocket;
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.channel = channel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,7 +52,7 @@ public class WebsocketHandle implements Closeable {
|
|||||||
public final MethodResult receive(Optional<Double> timeout) throws LuaException {
|
public final MethodResult receive(Optional<Double> timeout) throws LuaException {
|
||||||
checkOpen();
|
checkOpen();
|
||||||
var timeoutId = timeout.isPresent()
|
var timeoutId = timeout.isPresent()
|
||||||
? websocket.environment().startTimer(Math.round(checkFinite(0, timeout.get()) / 0.05))
|
? environment.startTimer(Math.round(checkFinite(0, timeout.get()) / 0.05))
|
||||||
: -1;
|
: -1;
|
||||||
|
|
||||||
return new ReceiveCallback(timeoutId).pull;
|
return new ReceiveCallback(timeoutId).pull;
|
||||||
@@ -78,17 +72,14 @@ public class WebsocketHandle implements Closeable {
|
|||||||
checkOpen();
|
checkOpen();
|
||||||
|
|
||||||
var text = message.value();
|
var text = message.value();
|
||||||
if (options.websocketMessage != 0 && text.length() > options.websocketMessage) {
|
if (options.websocketMessage() != 0 && text.length() > options.websocketMessage()) {
|
||||||
throw new LuaException("Message is too large");
|
throw new LuaException("Message is too large");
|
||||||
}
|
}
|
||||||
|
|
||||||
websocket.environment().observe(Metrics.WEBSOCKET_OUTGOING, text.length());
|
if (binary.orElse(false)) {
|
||||||
|
websocket.sendBinary(LuaValues.encode(text));
|
||||||
var channel = this.channel;
|
} else {
|
||||||
if (channel != null) {
|
websocket.sendText(text);
|
||||||
channel.writeAndFlush(binary.orElse(false)
|
|
||||||
? new BinaryWebSocketFrame(Unpooled.wrappedBuffer(LuaValues.encode(text)))
|
|
||||||
: new TextWebSocketFrame(text));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,25 +87,13 @@ public class WebsocketHandle implements Closeable {
|
|||||||
* Close this websocket. This will terminate the connection, meaning messages can no longer be sent or received
|
* Close this websocket. This will terminate the connection, meaning messages can no longer be sent or received
|
||||||
* along it.
|
* along it.
|
||||||
*/
|
*/
|
||||||
@LuaFunction("close")
|
@LuaFunction
|
||||||
public final void doClose() {
|
public final void close() {
|
||||||
close();
|
|
||||||
websocket.close();
|
websocket.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkOpen() throws LuaException {
|
private void checkOpen() throws LuaException {
|
||||||
if (closed) throw new LuaException("attempt to use a closed file");
|
if (websocket.isClosed()) throw new LuaException("attempt to use a closed file");
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
closed = true;
|
|
||||||
|
|
||||||
var channel = this.channel;
|
|
||||||
if (channel != null) {
|
|
||||||
channel.close();
|
|
||||||
this.channel = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class ReceiveCallback implements ILuaCallback {
|
private final class ReceiveCallback implements ILuaCallback {
|
||||||
@@ -127,9 +106,9 @@ public class WebsocketHandle implements Closeable {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MethodResult resume(Object[] event) {
|
public MethodResult resume(Object[] event) {
|
||||||
if (event.length >= 3 && Objects.equal(event[0], MESSAGE_EVENT) && Objects.equal(event[1], websocket.address())) {
|
if (event.length >= 3 && Objects.equal(event[0], MESSAGE_EVENT) && Objects.equal(event[1], address)) {
|
||||||
return MethodResult.of(Arrays.copyOfRange(event, 2, event.length));
|
return MethodResult.of(Arrays.copyOfRange(event, 2, event.length));
|
||||||
} else if (event.length >= 2 && Objects.equal(event[0], CLOSE_EVENT) && Objects.equal(event[1], websocket.address()) && closed) {
|
} else if (event.length >= 2 && Objects.equal(event[0], CLOSE_EVENT) && Objects.equal(event[1], address) && websocket.isClosed()) {
|
||||||
// If the socket is closed abort.
|
// If the socket is closed abort.
|
||||||
return MethodResult.of();
|
return MethodResult.of();
|
||||||
} else if (event.length >= 2 && timeoutId != -1 && Objects.equal(event[0], TIMER_EVENT)
|
} else if (event.length >= 2 && timeoutId != -1 && Objects.equal(event[0], TIMER_EVENT)
|
||||||
|
@@ -13,14 +13,14 @@ import io.netty.handler.codec.http.FullHttpResponse;
|
|||||||
import io.netty.handler.codec.http.websocketx.*;
|
import io.netty.handler.codec.http.websocketx.*;
|
||||||
import io.netty.util.CharsetUtil;
|
import io.netty.util.CharsetUtil;
|
||||||
|
|
||||||
import static dan200.computercraft.core.apis.http.websocket.Websocket.MESSAGE_EVENT;
|
import static dan200.computercraft.core.apis.http.websocket.WebsocketClient.MESSAGE_EVENT;
|
||||||
|
|
||||||
public class WebsocketHandler extends SimpleChannelInboundHandler<Object> {
|
class WebsocketHandler extends SimpleChannelInboundHandler<Object> {
|
||||||
private final Websocket websocket;
|
private final Websocket websocket;
|
||||||
private final Options options;
|
private final Options options;
|
||||||
private boolean handshakeComplete = false;
|
private boolean handshakeComplete = false;
|
||||||
|
|
||||||
public WebsocketHandler(Websocket websocket, Options options) {
|
WebsocketHandler(Websocket websocket, Options options) {
|
||||||
this.websocket = websocket;
|
this.websocket = websocket;
|
||||||
this.options = options;
|
this.options = options;
|
||||||
}
|
}
|
||||||
@@ -32,9 +32,9 @@ public class WebsocketHandler extends SimpleChannelInboundHandler<Object> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
|
||||||
if (evt == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_COMPLETE) {
|
if (evt == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_COMPLETE) {
|
||||||
websocket.success(ctx.channel(), options);
|
websocket.success(options);
|
||||||
handshakeComplete = true;
|
handshakeComplete = true;
|
||||||
} else if (evt == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_TIMEOUT) {
|
} else if (evt == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_TIMEOUT) {
|
||||||
websocket.failure("Timed out");
|
websocket.failure("Timed out");
|
||||||
|
@@ -2,14 +2,13 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package dan200.computercraft.shared.computer.upload;
|
package dan200.computercraft.core.apis.transfer;
|
||||||
|
|
||||||
import dan200.computercraft.api.lua.LuaFunction;
|
import dan200.computercraft.api.lua.LuaFunction;
|
||||||
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
|
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
|
||||||
import dan200.computercraft.core.apis.handles.ByteBufferChannel;
|
|
||||||
import dan200.computercraft.core.methods.ObjectSource;
|
import dan200.computercraft.core.methods.ObjectSource;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.channels.SeekableByteChannel;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -26,9 +25,9 @@ public class TransferredFile implements ObjectSource {
|
|||||||
private final String name;
|
private final String name;
|
||||||
private final BinaryReadableHandle handle;
|
private final BinaryReadableHandle handle;
|
||||||
|
|
||||||
public TransferredFile(String name, ByteBuffer contents) {
|
public TransferredFile(String name, SeekableByteChannel contents) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
handle = BinaryReadableHandle.of(new ByteBufferChannel(contents));
|
handle = BinaryReadableHandle.of(contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
@@ -2,13 +2,9 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package dan200.computercraft.shared.computer.upload;
|
package dan200.computercraft.core.apis.transfer;
|
||||||
|
|
||||||
import dan200.computercraft.api.lua.LuaFunction;
|
import dan200.computercraft.api.lua.LuaFunction;
|
||||||
import dan200.computercraft.shared.network.client.UploadResultMessage;
|
|
||||||
import dan200.computercraft.shared.platform.PlatformHelper;
|
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
|
||||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
@@ -19,16 +15,16 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
* @cc.module [kind=event] file_transfer.TransferredFiles
|
* @cc.module [kind=event] file_transfer.TransferredFiles
|
||||||
*/
|
*/
|
||||||
public class TransferredFiles {
|
public class TransferredFiles {
|
||||||
private final ServerPlayer player;
|
public static final String EVENT = "file_transfer";
|
||||||
private final AbstractContainerMenu container;
|
|
||||||
private final AtomicBoolean consumed = new AtomicBoolean(false);
|
private final AtomicBoolean consumed = new AtomicBoolean(false);
|
||||||
|
private final Runnable onConsumed;
|
||||||
|
|
||||||
private final List<TransferredFile> files;
|
private final List<TransferredFile> files;
|
||||||
|
|
||||||
public TransferredFiles(ServerPlayer player, AbstractContainerMenu container, List<TransferredFile> files) {
|
public TransferredFiles(List<TransferredFile> files, Runnable onConsumed) {
|
||||||
this.player = player;
|
|
||||||
this.container = container;
|
|
||||||
this.files = files;
|
this.files = files;
|
||||||
|
this.onConsumed = onConsumed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,9 +40,6 @@ public class TransferredFiles {
|
|||||||
|
|
||||||
private void consumed() {
|
private void consumed() {
|
||||||
if (consumed.getAndSet(true)) return;
|
if (consumed.getAndSet(true)) return;
|
||||||
|
onConsumed.run();
|
||||||
if (player.isAlive() && player.containerMenu == container) {
|
|
||||||
PlatformHelper.get().sendToPlayer(UploadResultMessage.consumed(container), player);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -20,7 +20,7 @@ import java.util.Objects;
|
|||||||
* This is used by {@link ComputerContext} to construct {@linkplain ComputerContext#peripheralMethods() the context-wide
|
* This is used by {@link ComputerContext} to construct {@linkplain ComputerContext#peripheralMethods() the context-wide
|
||||||
* method supplier}. It should not be used directly.
|
* method supplier}. It should not be used directly.
|
||||||
*/
|
*/
|
||||||
public class PeripheralMethodSupplier {
|
public final class PeripheralMethodSupplier {
|
||||||
private static final Generator<PeripheralMethod> GENERATOR = new Generator<>(PeripheralMethod.class, List.of(ILuaContext.class, IComputerAccess.class),
|
private static final Generator<PeripheralMethod> GENERATOR = new Generator<>(PeripheralMethod.class, List.of(ILuaContext.class, IComputerAccess.class),
|
||||||
m -> (target, context, computer, args) -> {
|
m -> (target, context, computer, args) -> {
|
||||||
var escArgs = args.escapes();
|
var escArgs = args.escapes();
|
||||||
@@ -31,6 +31,9 @@ public class PeripheralMethodSupplier {
|
|||||||
method -> (instance, context, computer, args) -> ((IDynamicPeripheral) instance).callMethod(computer, context, method, args)
|
method -> (instance, context, computer, args) -> ((IDynamicPeripheral) instance).callMethod(computer, context, method, args)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private PeripheralMethodSupplier() {
|
||||||
|
}
|
||||||
|
|
||||||
public static MethodSupplier<PeripheralMethod> create(List<GenericMethod> genericMethods) {
|
public static MethodSupplier<PeripheralMethod> create(List<GenericMethod> genericMethods) {
|
||||||
return new MethodSupplierImpl<>(genericMethods, GENERATOR, DYNAMIC, x -> x instanceof IDynamicPeripheral dynamic
|
return new MethodSupplierImpl<>(genericMethods, GENERATOR, DYNAMIC, x -> x instanceof IDynamicPeripheral dynamic
|
||||||
? Objects.requireNonNull(dynamic.getMethodNames(), "Dynamic methods cannot be null")
|
? Objects.requireNonNull(dynamic.getMethodNames(), "Dynamic methods cannot be null")
|
||||||
|
@@ -0,0 +1,146 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.filesystem;
|
||||||
|
|
||||||
|
import dan200.computercraft.api.filesystem.FileAttributes;
|
||||||
|
import dan200.computercraft.api.filesystem.FileOperationException;
|
||||||
|
import dan200.computercraft.api.filesystem.Mount;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.channels.SeekableByteChannel;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstract mount which stores its file tree in memory.
|
||||||
|
*
|
||||||
|
* @param <T> The type of file.
|
||||||
|
*/
|
||||||
|
public abstract class AbstractInMemoryMount<T extends AbstractInMemoryMount.FileEntry<T>> implements Mount {
|
||||||
|
protected static final String NO_SUCH_FILE = "No such file";
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
protected T root;
|
||||||
|
|
||||||
|
protected final @Nullable T get(String path) {
|
||||||
|
var lastEntry = root;
|
||||||
|
var lastIndex = 0;
|
||||||
|
|
||||||
|
while (lastEntry != null && lastIndex < path.length()) {
|
||||||
|
var nextIndex = path.indexOf('/', lastIndex);
|
||||||
|
if (nextIndex < 0) nextIndex = path.length();
|
||||||
|
|
||||||
|
lastEntry = lastEntry.children == null ? null : lastEntry.children.get(path.substring(lastIndex, nextIndex));
|
||||||
|
lastIndex = nextIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final boolean exists(String path) {
|
||||||
|
return get(path) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final boolean isDirectory(String path) {
|
||||||
|
var file = get(path);
|
||||||
|
return file != null && file.isDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void list(String path, List<String> contents) throws IOException {
|
||||||
|
var file = get(path);
|
||||||
|
if (file == null || file.children == null) throw new FileOperationException(path, "Not a directory");
|
||||||
|
|
||||||
|
contents.addAll(file.children.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final long getSize(String path) throws IOException {
|
||||||
|
var file = get(path);
|
||||||
|
if (file == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||||
|
return getSize(path, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the size of a file.
|
||||||
|
*
|
||||||
|
* @param path The file path, for error messages.
|
||||||
|
* @param file The file to get the size of.
|
||||||
|
* @return The size of the file. This should be 0 for directories, and equal to {@code openForRead(_).size()} for files.
|
||||||
|
* @throws IOException If the size could not be read.
|
||||||
|
*/
|
||||||
|
protected abstract long getSize(String path, T file) throws IOException;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final SeekableByteChannel openForRead(String path) throws IOException {
|
||||||
|
var file = get(path);
|
||||||
|
if (file == null || file.isDirectory()) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||||
|
return openForRead(path, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a file for reading.
|
||||||
|
*
|
||||||
|
* @param path The file path, for error messages.
|
||||||
|
* @param file The file to read. This will not be a directory.
|
||||||
|
* @return The channel for this file.
|
||||||
|
*/
|
||||||
|
protected abstract SeekableByteChannel openForRead(String path, T file) throws IOException;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final BasicFileAttributes getAttributes(String path) throws IOException {
|
||||||
|
var file = get(path);
|
||||||
|
if (file == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||||
|
return getAttributes(path, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all attributes of the file.
|
||||||
|
*
|
||||||
|
* @param path The file path, for error messages.
|
||||||
|
* @param file The file to compute attributes for.
|
||||||
|
* @return The file's attributes.
|
||||||
|
* @throws IOException If the attributes could not be read.
|
||||||
|
*/
|
||||||
|
protected BasicFileAttributes getAttributes(String path, T file) throws IOException {
|
||||||
|
return new FileAttributes(file.isDirectory(), getSize(path, file));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected T getOrCreateChild(T lastEntry, String localPath, Function<String, T> factory) {
|
||||||
|
var lastIndex = 0;
|
||||||
|
while (lastIndex < localPath.length()) {
|
||||||
|
var nextIndex = localPath.indexOf('/', lastIndex);
|
||||||
|
if (nextIndex < 0) nextIndex = localPath.length();
|
||||||
|
|
||||||
|
var part = localPath.substring(lastIndex, nextIndex);
|
||||||
|
if (lastEntry.children == null) lastEntry.children = new HashMap<>(0);
|
||||||
|
|
||||||
|
var nextEntry = lastEntry.children.get(part);
|
||||||
|
if (nextEntry == null || !nextEntry.isDirectory()) {
|
||||||
|
lastEntry.children.put(part, nextEntry = factory.apply(localPath.substring(0, nextIndex)));
|
||||||
|
}
|
||||||
|
|
||||||
|
lastEntry = nextEntry;
|
||||||
|
lastIndex = nextIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class FileEntry<T extends FileEntry<T>> {
|
||||||
|
@Nullable
|
||||||
|
public Map<String, T> children;
|
||||||
|
|
||||||
|
public boolean isDirectory() {
|
||||||
|
return children != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -6,25 +6,21 @@ package dan200.computercraft.core.filesystem;
|
|||||||
|
|
||||||
import com.google.common.cache.Cache;
|
import com.google.common.cache.Cache;
|
||||||
import com.google.common.cache.CacheBuilder;
|
import com.google.common.cache.CacheBuilder;
|
||||||
import dan200.computercraft.api.filesystem.FileAttributes;
|
|
||||||
import dan200.computercraft.api.filesystem.FileOperationException;
|
|
||||||
import dan200.computercraft.api.filesystem.Mount;
|
|
||||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.channels.SeekableByteChannel;
|
import java.nio.channels.SeekableByteChannel;
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An abstract mount based on some archive of files, such as a Zip or Minecraft's resources.
|
* An abstract mount based on some archive of files, such as a Zip or Minecraft's resources.
|
||||||
|
* <p>
|
||||||
|
* We assume that we cannot create {@link SeekableByteChannel}s directly from the archive, and so maintain a (shared)
|
||||||
|
* cache of recently read files and their contents.
|
||||||
*
|
*
|
||||||
* @param <T> The type of file.
|
* @param <T> The type of file.
|
||||||
*/
|
*/
|
||||||
public abstract class ArchiveMount<T extends ArchiveMount.FileEntry<T>> implements Mount {
|
public abstract class ArchiveMount<T extends ArchiveMount.FileEntry<T>> extends AbstractInMemoryMount<T> {
|
||||||
protected static final String NO_SUCH_FILE = "No such file";
|
protected static final String NO_SUCH_FILE = "No such file";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,128 +40,51 @@ public abstract class ArchiveMount<T extends ArchiveMount.FileEntry<T>> implemen
|
|||||||
.<FileEntry<?>, byte[]>weigher((k, v) -> v.length)
|
.<FileEntry<?>, byte[]>weigher((k, v) -> v.length)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@Nullable
|
|
||||||
protected T root;
|
|
||||||
|
|
||||||
private @Nullable T get(String path) {
|
|
||||||
var lastEntry = root;
|
|
||||||
var lastIndex = 0;
|
|
||||||
|
|
||||||
while (lastEntry != null && lastIndex < path.length()) {
|
|
||||||
var nextIndex = path.indexOf('/', lastIndex);
|
|
||||||
if (nextIndex < 0) nextIndex = path.length();
|
|
||||||
|
|
||||||
lastEntry = lastEntry.children == null ? null : lastEntry.children.get(path.substring(lastIndex, nextIndex));
|
|
||||||
lastIndex = nextIndex + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return lastEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final boolean exists(String path) {
|
protected final long getSize(String path, T file) throws IOException {
|
||||||
return get(path) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final boolean isDirectory(String path) {
|
|
||||||
var file = get(path);
|
|
||||||
return file != null && file.isDirectory();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final void list(String path, List<String> contents) throws IOException {
|
|
||||||
var file = get(path);
|
|
||||||
if (file == null || !file.isDirectory()) throw new FileOperationException(path, "Not a directory");
|
|
||||||
|
|
||||||
file.list(contents);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final long getSize(String path) throws IOException {
|
|
||||||
var file = get(path);
|
|
||||||
if (file == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
|
||||||
return getCachedSize(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
private long getCachedSize(T file) throws IOException {
|
|
||||||
if (file.size != -1) return file.size;
|
if (file.size != -1) return file.size;
|
||||||
if (file.isDirectory()) return file.size = 0;
|
if (file.isDirectory()) return file.size = 0;
|
||||||
|
|
||||||
var contents = CONTENTS_CACHE.getIfPresent(file);
|
var contents = CONTENTS_CACHE.getIfPresent(file);
|
||||||
if (contents != null) return file.size = contents.length;
|
return file.size = contents != null ? contents.length : getFileSize(path, file);
|
||||||
|
|
||||||
return file.size = getSize(file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the size of a file.
|
* Get the size of the file by reading it (or its metadata) from disk.
|
||||||
* <p>
|
|
||||||
* This should only be called once per file, as the result is cached in {@link #getSize(String)}.
|
|
||||||
*
|
*
|
||||||
* @param file The file to compute the size of. This will not be a directory.
|
* @param path The file path, for error messages.
|
||||||
* @return The size of the file.
|
* @param file The file to get the size of.
|
||||||
* @throws IOException If the size could not be read.
|
* @return The file's size.
|
||||||
|
* @throws IOException If the size could not be computed.
|
||||||
*/
|
*/
|
||||||
protected abstract long getSize(T file) throws IOException;
|
protected long getFileSize(String path, T file) throws IOException {
|
||||||
|
return getContents(path, file).length;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SeekableByteChannel openForRead(String path) throws IOException {
|
protected final SeekableByteChannel openForRead(String path, T file) throws IOException {
|
||||||
var file = get(path);
|
return new ArrayByteChannel(getContents(path, file));
|
||||||
if (file == null || file.isDirectory()) throw new FileOperationException(path, NO_SUCH_FILE);
|
}
|
||||||
|
|
||||||
|
private byte[] getContents(String path, T file) throws IOException {
|
||||||
var cachedContents = CONTENTS_CACHE.getIfPresent(file);
|
var cachedContents = CONTENTS_CACHE.getIfPresent(file);
|
||||||
if (cachedContents != null) return new ArrayByteChannel(cachedContents);
|
if (cachedContents != null) return cachedContents;
|
||||||
|
|
||||||
var contents = getContents(file);
|
var contents = getFileContents(path, file);
|
||||||
CONTENTS_CACHE.put(file, contents);
|
CONTENTS_CACHE.put(file, contents);
|
||||||
return new ArrayByteChannel(contents);
|
return contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read the entirety of a file into memory.
|
* Read the entirety of a file into memory.
|
||||||
*
|
*
|
||||||
|
* @param path The file path, for error messages.
|
||||||
* @param file The file to read into memory. This will not be a directory.
|
* @param file The file to read into memory. This will not be a directory.
|
||||||
* @return The contents of the file.
|
* @return The contents of the file.
|
||||||
*/
|
*/
|
||||||
protected abstract byte[] getContents(T file) throws IOException;
|
protected abstract byte[] getFileContents(String path, T file) throws IOException;
|
||||||
|
|
||||||
@Override
|
|
||||||
public final BasicFileAttributes getAttributes(String path) throws IOException {
|
|
||||||
var file = get(path);
|
|
||||||
if (file == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
|
||||||
|
|
||||||
return getAttributes(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all attributes of the file.
|
|
||||||
*
|
|
||||||
* @param file The file to compute attributes for. This will not be a directory.
|
|
||||||
* @return The file's attributes.
|
|
||||||
* @throws IOException If the attributes could not be read.
|
|
||||||
*/
|
|
||||||
protected BasicFileAttributes getAttributes(T file) throws IOException {
|
|
||||||
return new FileAttributes(file.isDirectory(), getCachedSize(file));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static class FileEntry<T extends ArchiveMount.FileEntry<T>> {
|
|
||||||
public final String path;
|
|
||||||
@Nullable
|
|
||||||
public Map<String, T> children;
|
|
||||||
|
|
||||||
|
protected static class FileEntry<T extends FileEntry<T>> extends AbstractInMemoryMount.FileEntry<T> {
|
||||||
long size = -1;
|
long size = -1;
|
||||||
|
|
||||||
protected FileEntry(String path) {
|
|
||||||
this.path = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean isDirectory() {
|
|
||||||
return children != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void list(List<String> contents) {
|
|
||||||
if (children != null) contents.addAll(children.keySet());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
package dan200.computercraft.core.filesystem;
|
package dan200.computercraft.core.filesystem;
|
||||||
|
|
||||||
|
import dan200.computercraft.api.filesystem.FileAttributes;
|
||||||
import dan200.computercraft.api.filesystem.FileOperationException;
|
import dan200.computercraft.api.filesystem.FileOperationException;
|
||||||
import dan200.computercraft.core.util.Nullability;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
@@ -22,7 +22,7 @@ import java.util.zip.ZipFile;
|
|||||||
/**
|
/**
|
||||||
* A mount which reads zip/jar files.
|
* A mount which reads zip/jar files.
|
||||||
*/
|
*/
|
||||||
public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closeable {
|
public final class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closeable {
|
||||||
private final ZipFile zip;
|
private final ZipFile zip;
|
||||||
|
|
||||||
public JarMount(File jarFile, String subPath) throws IOException {
|
public JarMount(File jarFile, String subPath) throws IOException {
|
||||||
@@ -42,7 +42,7 @@ public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closea
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read in all the entries
|
// Read in all the entries
|
||||||
root = new FileEntry("");
|
var root = this.root = new FileEntry();
|
||||||
var zipEntries = zip.entries();
|
var zipEntries = zip.entries();
|
||||||
while (zipEntries.hasMoreElements()) {
|
while (zipEntries.hasMoreElements()) {
|
||||||
var entry = zipEntries.nextElement();
|
var entry = zipEntries.nextElement();
|
||||||
@@ -51,55 +51,33 @@ public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closea
|
|||||||
if (!entryPath.startsWith(subPath)) continue;
|
if (!entryPath.startsWith(subPath)) continue;
|
||||||
|
|
||||||
var localPath = FileSystem.toLocal(entryPath, subPath);
|
var localPath = FileSystem.toLocal(entryPath, subPath);
|
||||||
create(entry, localPath);
|
getOrCreateChild(root, localPath, x -> new FileEntry()).setup(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void create(ZipEntry entry, String localPath) {
|
|
||||||
var lastEntry = Nullability.assertNonNull(root);
|
|
||||||
|
|
||||||
var lastIndex = 0;
|
|
||||||
while (lastIndex < localPath.length()) {
|
|
||||||
var nextIndex = localPath.indexOf('/', lastIndex);
|
|
||||||
if (nextIndex < 0) nextIndex = localPath.length();
|
|
||||||
|
|
||||||
var part = localPath.substring(lastIndex, nextIndex);
|
|
||||||
if (lastEntry.children == null) lastEntry.children = new HashMap<>(0);
|
|
||||||
|
|
||||||
var nextEntry = lastEntry.children.get(part);
|
|
||||||
if (nextEntry == null || !nextEntry.isDirectory()) {
|
|
||||||
lastEntry.children.put(part, nextEntry = new FileEntry(localPath.substring(0, nextIndex)));
|
|
||||||
}
|
|
||||||
|
|
||||||
lastEntry = nextEntry;
|
|
||||||
lastIndex = nextIndex + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastEntry.setup(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected long getSize(FileEntry file) throws FileOperationException {
|
protected long getFileSize(String path, FileEntry file) throws FileOperationException {
|
||||||
if (file.zipEntry == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
|
if (file.zipEntry == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||||
return file.zipEntry.getSize();
|
return file.zipEntry.getSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected byte[] getContents(FileEntry file) throws FileOperationException {
|
protected byte[] getFileContents(String path, FileEntry file) throws FileOperationException {
|
||||||
if (file.zipEntry == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
|
if (file.zipEntry == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||||
|
|
||||||
try (var stream = zip.getInputStream(file.zipEntry)) {
|
try (var stream = zip.getInputStream(file.zipEntry)) {
|
||||||
return stream.readAllBytes();
|
return stream.readAllBytes();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// Mask other IO exceptions as a non-existent file.
|
// Mask other IO exceptions as a non-existent file.
|
||||||
throw new FileOperationException(file.path, NO_SUCH_FILE);
|
throw new FileOperationException(path, NO_SUCH_FILE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BasicFileAttributes getAttributes(FileEntry file) throws FileOperationException {
|
protected BasicFileAttributes getAttributes(String path, FileEntry file) throws IOException {
|
||||||
if (file.zipEntry == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
|
return file.zipEntry == null ? super.getAttributes(path, file) : new FileAttributes(
|
||||||
return new ZipEntryAttributes(file.zipEntry);
|
file.isDirectory(), getSize(path, file), orEpoch(file.zipEntry.getCreationTime()), orEpoch(file.zipEntry.getLastModifiedTime())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -107,79 +85,19 @@ public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closea
|
|||||||
zip.close();
|
zip.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static class FileEntry extends ArchiveMount.FileEntry<FileEntry> {
|
protected static final class FileEntry extends ArchiveMount.FileEntry<FileEntry> {
|
||||||
@Nullable
|
@Nullable
|
||||||
ZipEntry zipEntry;
|
ZipEntry zipEntry;
|
||||||
|
|
||||||
protected FileEntry(String path) {
|
|
||||||
super(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setup(ZipEntry entry) {
|
void setup(ZipEntry entry) {
|
||||||
zipEntry = entry;
|
zipEntry = entry;
|
||||||
size = entry.getSize();
|
|
||||||
if (children == null && entry.isDirectory()) children = new HashMap<>(0);
|
if (children == null && entry.isDirectory()) children = new HashMap<>(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class ZipEntryAttributes implements BasicFileAttributes {
|
private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
|
||||||
private final ZipEntry entry;
|
|
||||||
|
|
||||||
ZipEntryAttributes(ZipEntry entry) {
|
private static FileTime orEpoch(@Nullable FileTime time) {
|
||||||
this.entry = entry;
|
return time == null ? EPOCH : time;
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FileTime lastModifiedTime() {
|
|
||||||
return orEpoch(entry.getLastModifiedTime());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FileTime lastAccessTime() {
|
|
||||||
return orEpoch(entry.getLastAccessTime());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FileTime creationTime() {
|
|
||||||
var time = entry.getCreationTime();
|
|
||||||
return time == null ? lastModifiedTime() : time;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isRegularFile() {
|
|
||||||
return !entry.isDirectory();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isDirectory() {
|
|
||||||
return entry.isDirectory();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isSymbolicLink() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isOther() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long size() {
|
|
||||||
return entry.getSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public Object fileKey() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
|
|
||||||
|
|
||||||
private static FileTime orEpoch(@Nullable FileTime time) {
|
|
||||||
return time == null ? EPOCH : time;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,334 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.filesystem;
|
||||||
|
|
||||||
|
import dan200.computercraft.api.filesystem.FileAttributes;
|
||||||
|
import dan200.computercraft.api.filesystem.FileOperationException;
|
||||||
|
import dan200.computercraft.api.filesystem.Mount;
|
||||||
|
import dan200.computercraft.api.filesystem.WritableMount;
|
||||||
|
import dan200.computercraft.core.util.Nullability;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.ClosedChannelException;
|
||||||
|
import java.nio.channels.SeekableByteChannel;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.AccessDeniedException;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.nio.file.attribute.FileTime;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import static dan200.computercraft.core.filesystem.WritableFileMount.MINIMUM_FILE_SIZE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A basic {@link Mount} which stores files and directories in-memory.
|
||||||
|
*/
|
||||||
|
public final class MemoryMount extends AbstractInMemoryMount<MemoryMount.FileEntry> implements WritableMount {
|
||||||
|
private static final byte[] EMPTY = new byte[0];
|
||||||
|
private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
|
||||||
|
|
||||||
|
private final long capacity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a memory mount with a 1GB capacity.
|
||||||
|
*/
|
||||||
|
public MemoryMount() {
|
||||||
|
this(1000_000_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a memory mount with a custom capacity. Note, this is only used in calculations for {@link #getCapacity()}
|
||||||
|
* and {@link #getRemainingSpace()}, it is not checked when creating or writing files.
|
||||||
|
*
|
||||||
|
* @param capacity The capacity of this mount.
|
||||||
|
*/
|
||||||
|
public MemoryMount(long capacity) {
|
||||||
|
this.capacity = capacity;
|
||||||
|
root = new FileEntry();
|
||||||
|
root.children = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public MemoryMount addFile(String file, byte[] contents, FileTime created, FileTime modified) {
|
||||||
|
var entry = getOrCreateChild(Nullability.assertNonNull(root), file, x -> new FileEntry());
|
||||||
|
entry.contents = contents;
|
||||||
|
entry.length = contents.length;
|
||||||
|
entry.created = created;
|
||||||
|
entry.modified = modified;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MemoryMount addFile(String file, String contents, FileTime created, FileTime modified) {
|
||||||
|
return addFile(file, contents.getBytes(StandardCharsets.UTF_8), created, modified);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MemoryMount addFile(String file, byte[] contents) {
|
||||||
|
return addFile(file, contents, EPOCH, EPOCH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MemoryMount addFile(String file, String contents) {
|
||||||
|
return addFile(file, contents, EPOCH, EPOCH);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected long getSize(String path, FileEntry file) {
|
||||||
|
return file.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected SeekableByteChannel openForRead(String path, FileEntry file) throws IOException {
|
||||||
|
if (file.contents == null) throw new FileOperationException(path, "File is a directory");
|
||||||
|
return new EntryChannel(file, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected BasicFileAttributes getAttributes(String path, FileEntry file) throws IOException {
|
||||||
|
return new FileAttributes(file.isDirectory(), file.length, file.created, file.modified);
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable ParentAndName getParentAndName(String path) {
|
||||||
|
if (path.isEmpty()) throw new IllegalArgumentException("Path is empty");
|
||||||
|
var index = path.lastIndexOf('/');
|
||||||
|
if (index == -1) {
|
||||||
|
return new ParentAndName(Nullability.assertNonNull(Nullability.assertNonNull(root).children), path);
|
||||||
|
}
|
||||||
|
|
||||||
|
var entry = get(path.substring(0, index));
|
||||||
|
return entry == null || entry.children == null
|
||||||
|
? null
|
||||||
|
: new ParentAndName(entry.children, path.substring(index + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void makeDirectory(String path) throws IOException {
|
||||||
|
if (path.isEmpty()) return;
|
||||||
|
|
||||||
|
var lastEntry = Nullability.assertNonNull(root);
|
||||||
|
var lastIndex = 0;
|
||||||
|
while (lastIndex < path.length()) {
|
||||||
|
if (lastEntry.children == null) throw new NullPointerException("children is null");
|
||||||
|
|
||||||
|
var nextIndex = path.indexOf('/', lastIndex);
|
||||||
|
if (nextIndex < 0) nextIndex = path.length();
|
||||||
|
|
||||||
|
var part = path.substring(lastIndex, nextIndex);
|
||||||
|
var nextEntry = lastEntry.children.get(part);
|
||||||
|
if (nextEntry == null) {
|
||||||
|
lastEntry.children.put(part, nextEntry = FileEntry.newDir());
|
||||||
|
} else if (nextEntry.children == null) {
|
||||||
|
throw new FileOperationException(path, "File exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
lastEntry = nextEntry;
|
||||||
|
lastIndex = nextIndex + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(String path) throws IOException {
|
||||||
|
if (path.isEmpty()) throw new AccessDeniedException("Access denied");
|
||||||
|
var node = getParentAndName(path);
|
||||||
|
if (node != null) node.parent().remove(node.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void rename(String source, String dest) throws IOException {
|
||||||
|
if (dest.startsWith(source)) throw new FileOperationException(source, "Cannot move a directory inside itself");
|
||||||
|
|
||||||
|
var sourceParent = getParentAndName(source);
|
||||||
|
if (sourceParent == null || !sourceParent.exists()) throw new FileOperationException(source, "No such file");
|
||||||
|
|
||||||
|
var destParent = getParentAndName(dest);
|
||||||
|
if (destParent == null) throw new FileOperationException(dest, "Parent directory does not exist");
|
||||||
|
if (destParent.exists()) throw new FileOperationException(dest, "File exists");
|
||||||
|
|
||||||
|
destParent.put(sourceParent.parent().remove(sourceParent.name()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileEntry getForWrite(String path) throws FileOperationException {
|
||||||
|
if (path.isEmpty()) throw new FileOperationException(path, "Cannot write to directory");
|
||||||
|
|
||||||
|
var parent = getParentAndName(path);
|
||||||
|
if (parent == null) throw new FileOperationException(path, "Parent directory does not exist");
|
||||||
|
|
||||||
|
var file = parent.get();
|
||||||
|
if (file != null && file.isDirectory()) throw new FileOperationException(path, "Cannot write to directory");
|
||||||
|
if (file == null) parent.put(file = FileEntry.newFile());
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SeekableByteChannel openForWrite(String path) throws IOException {
|
||||||
|
var file = getForWrite(path);
|
||||||
|
|
||||||
|
// Truncate the file.
|
||||||
|
file.contents = EMPTY;
|
||||||
|
file.length = 0;
|
||||||
|
return new EntryChannel(file, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SeekableByteChannel openForAppend(String path) throws IOException {
|
||||||
|
var file = getForWrite(path);
|
||||||
|
return new EntryChannel(file, file.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getRemainingSpace() {
|
||||||
|
return capacity - computeUsedSpace();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getCapacity() {
|
||||||
|
return capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long computeUsedSpace() {
|
||||||
|
Queue<FileEntry> queue = new ArrayDeque<>();
|
||||||
|
queue.add(root);
|
||||||
|
|
||||||
|
long size = 0;
|
||||||
|
|
||||||
|
FileEntry entry;
|
||||||
|
while ((entry = queue.poll()) != null) {
|
||||||
|
if (entry.children == null) {
|
||||||
|
size += Math.max(MINIMUM_FILE_SIZE, Nullability.assertNonNull(entry.contents).length);
|
||||||
|
} else {
|
||||||
|
size += MINIMUM_FILE_SIZE;
|
||||||
|
queue.addAll(entry.children.values());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return size - MINIMUM_FILE_SIZE; // Subtract one file for the root.
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static final class FileEntry extends AbstractInMemoryMount.FileEntry<FileEntry> {
|
||||||
|
FileTime created = EPOCH;
|
||||||
|
FileTime modified = EPOCH;
|
||||||
|
@Nullable
|
||||||
|
byte[] contents;
|
||||||
|
|
||||||
|
int length;
|
||||||
|
|
||||||
|
static FileEntry newFile() {
|
||||||
|
var entry = new FileEntry();
|
||||||
|
entry.contents = EMPTY;
|
||||||
|
entry.created = entry.modified = FileTime.from(Instant.now());
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
static FileEntry newDir() {
|
||||||
|
var entry = new FileEntry();
|
||||||
|
entry.children = new HashMap<>();
|
||||||
|
entry.created = entry.modified = FileTime.from(Instant.now());
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ParentAndName(Map<String, FileEntry> parent, String name) {
|
||||||
|
boolean exists() {
|
||||||
|
return parent.containsKey(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
FileEntry get() {
|
||||||
|
return parent.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
void put(FileEntry entry) {
|
||||||
|
assert !parent.containsKey(name);
|
||||||
|
parent.put(name, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class EntryChannel implements SeekableByteChannel {
|
||||||
|
private final FileEntry entry;
|
||||||
|
private long position;
|
||||||
|
private boolean isOpen = true;
|
||||||
|
|
||||||
|
private void checkClosed() throws ClosedChannelException {
|
||||||
|
if (!isOpen()) throw new ClosedChannelException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private EntryChannel(FileEntry entry, int position) {
|
||||||
|
this.entry = entry;
|
||||||
|
this.position = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(ByteBuffer destination) throws IOException {
|
||||||
|
checkClosed();
|
||||||
|
|
||||||
|
var backing = Nullability.assertNonNull(entry.contents);
|
||||||
|
if (position >= backing.length) return -1;
|
||||||
|
|
||||||
|
var remaining = Math.min(backing.length - (int) position, destination.remaining());
|
||||||
|
destination.put(backing, (int) position, remaining);
|
||||||
|
position += remaining;
|
||||||
|
return remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] ensureCapacity(int capacity) {
|
||||||
|
var contents = Nullability.assertNonNull(entry.contents);
|
||||||
|
if (capacity >= entry.length) {
|
||||||
|
var newCapacity = Math.max(capacity, contents.length << 1);
|
||||||
|
contents = entry.contents = Arrays.copyOf(contents, newCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int write(ByteBuffer src) throws IOException {
|
||||||
|
var toWrite = src.remaining();
|
||||||
|
var endPosition = position + toWrite;
|
||||||
|
if (endPosition > 1 << 30) throw new IOException("File is too large");
|
||||||
|
|
||||||
|
var contents = ensureCapacity((int) endPosition);
|
||||||
|
src.get(contents, (int) position, toWrite);
|
||||||
|
position = endPosition;
|
||||||
|
if (endPosition > entry.length) entry.length = (int) endPosition;
|
||||||
|
return toWrite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long position() throws IOException {
|
||||||
|
checkClosed();
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SeekableByteChannel position(long newPosition) throws IOException {
|
||||||
|
checkClosed();
|
||||||
|
if (newPosition < 0) throw new IllegalArgumentException("Position out of bounds");
|
||||||
|
this.position = newPosition;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long size() throws IOException {
|
||||||
|
checkClosed();
|
||||||
|
return entry.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SeekableByteChannel truncate(long size) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOpen() {
|
||||||
|
return isOpen && entry.contents != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
checkClosed();
|
||||||
|
isOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -27,7 +27,7 @@ import java.util.Set;
|
|||||||
public class WritableFileMount extends FileMount implements WritableMount {
|
public class WritableFileMount extends FileMount implements WritableMount {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(WritableFileMount.class);
|
private static final Logger LOG = LoggerFactory.getLogger(WritableFileMount.class);
|
||||||
|
|
||||||
private static final long MINIMUM_FILE_SIZE = 500;
|
static final long MINIMUM_FILE_SIZE = 500;
|
||||||
private static final Set<OpenOption> WRITE_OPTIONS = Sets.newHashSet(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
private static final Set<OpenOption> WRITE_OPTIONS = Sets.newHashSet(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||||
private static final Set<OpenOption> APPEND_OPTIONS = Sets.newHashSet(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
|
private static final Set<OpenOption> APPEND_OPTIONS = Sets.newHashSet(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
|
||||||
|
|
||||||
|
@@ -687,7 +687,7 @@ settings.define("paint.default_extension", {
|
|||||||
|
|
||||||
settings.define("list.show_hidden", {
|
settings.define("list.show_hidden", {
|
||||||
default = false,
|
default = false,
|
||||||
description = [[Show hidden files (those starting with "." in the Lua REPL).]],
|
description = [[Whether the list program show hidden files (those starting with ".").]],
|
||||||
type = "boolean",
|
type = "boolean",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -17,7 +17,7 @@ instance, `commands.say("Hi!")` is equivalent to `commands.exec("say Hi!")`.
|
|||||||
commands. `commands.async.say("Hi!")` is equivalent to
|
commands. `commands.async.say("Hi!")` is equivalent to
|
||||||
`commands.execAsync("say Hi!")`.
|
`commands.execAsync("say Hi!")`.
|
||||||
|
|
||||||
[mc]: https://minecraft.gamepedia.com/Commands
|
[mc]: https://minecraft.wiki/w/Commands
|
||||||
|
|
||||||
@module commands
|
@module commands
|
||||||
@usage Set the block above this computer to stone:
|
@usage Set the block above this computer to stone:
|
||||||
|
@@ -96,7 +96,7 @@ end
|
|||||||
--
|
--
|
||||||
-- If this returns true, you will can [play][`disk.playAudio`] the record.
|
-- If this returns true, you will can [play][`disk.playAudio`] the record.
|
||||||
--
|
--
|
||||||
-- [disk]: https://minecraft.gamepedia.com/Music_Disc
|
-- [disk]: https://minecraft.wiki/w/Music_Disc
|
||||||
--
|
--
|
||||||
-- @tparam string name The name of the disk drive.
|
-- @tparam string name The name of the disk drive.
|
||||||
-- @treturn boolean If the disk is present and has audio saved on it.
|
-- @treturn boolean If the disk is present and has audio saved on it.
|
||||||
|
@@ -722,7 +722,7 @@ do
|
|||||||
- `parse_empty_array`: When false, empty arrays will be parsed as a new table.
|
- `parse_empty_array`: When false, empty arrays will be parsed as a new table.
|
||||||
By default (or when this value is true), they are parsed as [`empty_json_array`].
|
By default (or when this value is true), they are parsed as [`empty_json_array`].
|
||||||
|
|
||||||
[nbt]: https://minecraft.gamepedia.com/NBT_format
|
[nbt]: https://minecraft.wiki/w/NBT_format
|
||||||
@return[1] The deserialised object
|
@return[1] The deserialised object
|
||||||
@treturn[2] nil If the object could not be deserialised.
|
@treturn[2] nil If the object could not be deserialised.
|
||||||
@treturn string A message describing why the JSON string is invalid.
|
@treturn string A message describing why the JSON string is invalid.
|
||||||
|
@@ -1,3 +1,12 @@
|
|||||||
|
# New features in CC: Tweaked 1.108.2
|
||||||
|
|
||||||
|
* Add a tag for which blocks wired modems should ignore.
|
||||||
|
|
||||||
|
Several bug fixes:
|
||||||
|
* Fix monitors sometimes being warped after resizing.
|
||||||
|
* Fix the skull recipes using the wrong UUID format.
|
||||||
|
* Fix paint canvas not always being redrawn after a term resize.
|
||||||
|
|
||||||
# New features in CC: Tweaked 1.108.1
|
# New features in CC: Tweaked 1.108.1
|
||||||
|
|
||||||
Several bug fixes:
|
Several bug fixes:
|
||||||
|
@@ -12,4 +12,4 @@ commands.give( "dan200", "minecraft:diamond", 64 )
|
|||||||
This works with any command. Use "commands.async" instead of "commands" to execute asynchronously.
|
This works with any command. Use "commands.async" instead of "commands" to execute asynchronously.
|
||||||
|
|
||||||
The commands API is only available on Command Computers.
|
The commands API is only available on Command Computers.
|
||||||
Visit http://minecraft.gamepedia.com/Commands for documentation on all commands.
|
Visit https://minecraft.wiki/w/Commands for documentation on all commands.
|
||||||
|
@@ -6,4 +6,4 @@ if sEvent == "key" and nKey == keys.enter then
|
|||||||
-- Do something
|
-- Do something
|
||||||
end
|
end
|
||||||
|
|
||||||
See http://www.minecraftwiki.net/wiki/Key_codes, or the source code, for a complete reference.
|
See https://www.minecraft.wiki/w/Key_codes, or the source code, for a complete reference.
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
New features in CC: Tweaked 1.108.1
|
New features in CC: Tweaked 1.108.2
|
||||||
|
|
||||||
|
* Add a tag for which blocks wired modems should ignore.
|
||||||
|
|
||||||
Several bug fixes:
|
Several bug fixes:
|
||||||
* Prevent no-opped players breaking or placing command computers.
|
* Fix monitors sometimes being warped after resizing.
|
||||||
* Allow using `@LuaFunction`-annotated methods on classes defined in child classloaders.
|
* Fix the skull recipes using the wrong UUID format.
|
||||||
|
* Fix paint canvas not always being redrawn after a term resize.
|
||||||
|
|
||||||
Type "help changelog" to see the full version history.
|
Type "help changelog" to see the full version history.
|
||||||
|
@@ -275,6 +275,12 @@ local function drawCanvas()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function termResize()
|
||||||
|
w, h = term.getSize()
|
||||||
|
drawCanvas()
|
||||||
|
drawInterface()
|
||||||
|
end
|
||||||
|
|
||||||
local menu_choices = {
|
local menu_choices = {
|
||||||
Save = function()
|
Save = function()
|
||||||
if bReadOnly then
|
if bReadOnly then
|
||||||
@@ -376,6 +382,8 @@ local function accessMenu()
|
|||||||
nMenuPosEnd = nMenuPosEnd + 1
|
nMenuPosEnd = nMenuPosEnd + 1
|
||||||
nMenuPosStart = nMenuPosEnd
|
nMenuPosStart = nMenuPosEnd
|
||||||
end
|
end
|
||||||
|
elseif id == "term_resize" then
|
||||||
|
termResize()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -434,9 +442,7 @@ local function handleEvents()
|
|||||||
drawInterface()
|
drawInterface()
|
||||||
end
|
end
|
||||||
elseif id == "term_resize" then
|
elseif id == "term_resize" then
|
||||||
w, h = term.getSize()
|
termResize()
|
||||||
drawCanvas()
|
|
||||||
drawInterface()
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@@ -23,8 +23,8 @@ public class AddressRuleTest {
|
|||||||
Action.ALLOW.toPartial()
|
Action.ALLOW.toPartial()
|
||||||
));
|
));
|
||||||
|
|
||||||
assertEquals(apply(rules, "localhost", 8080).action, Action.ALLOW);
|
assertEquals(apply(rules, "localhost", 8080).action(), Action.ALLOW);
|
||||||
assertEquals(apply(rules, "localhost", 8081).action, Action.DENY);
|
assertEquals(apply(rules, "localhost", 8081).action(), Action.DENY);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@@ -43,7 +43,7 @@ public class AddressRuleTest {
|
|||||||
"169.254.169.254", // AWS, Digital Ocean, GCP, etc..
|
"169.254.169.254", // AWS, Digital Ocean, GCP, etc..
|
||||||
})
|
})
|
||||||
public void blocksLocalDomains(String domain) {
|
public void blocksLocalDomains(String domain) {
|
||||||
assertEquals(apply(CoreConfig.httpRules, domain, 80).action, Action.DENY);
|
assertEquals(apply(CoreConfig.httpRules, domain, 80).action(), Action.DENY);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@@ -52,7 +52,7 @@ public class AddressRuleTest {
|
|||||||
"100.63.255.255", "100.128.0.0"
|
"100.63.255.255", "100.128.0.0"
|
||||||
})
|
})
|
||||||
public void allowsNonLocalDomains(String domain) {
|
public void allowsNonLocalDomains(String domain) {
|
||||||
assertEquals(apply(CoreConfig.httpRules, domain, 80).action, Action.ALLOW);
|
assertEquals(apply(CoreConfig.httpRules, domain, 80).action(), Action.ALLOW);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Options apply(Iterable<AddressRule> rules, String host, int port) {
|
private Options apply(Iterable<AddressRule> rules, String host, int port) {
|
||||||
|
@@ -12,10 +12,9 @@ import dan200.computercraft.api.lua.LuaFunction;
|
|||||||
import dan200.computercraft.core.ComputerContext;
|
import dan200.computercraft.core.ComputerContext;
|
||||||
import dan200.computercraft.core.computer.mainthread.MainThread;
|
import dan200.computercraft.core.computer.mainthread.MainThread;
|
||||||
import dan200.computercraft.core.computer.mainthread.MainThreadConfig;
|
import dan200.computercraft.core.computer.mainthread.MainThreadConfig;
|
||||||
|
import dan200.computercraft.core.filesystem.MemoryMount;
|
||||||
import dan200.computercraft.core.terminal.Terminal;
|
import dan200.computercraft.core.terminal.Terminal;
|
||||||
import dan200.computercraft.test.core.computer.BasicEnvironment;
|
import dan200.computercraft.test.core.computer.BasicEnvironment;
|
||||||
import dan200.computercraft.test.core.filesystem.MemoryMount;
|
|
||||||
import dan200.computercraft.test.core.filesystem.ReadOnlyWritableMount;
|
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -37,7 +36,7 @@ public class ComputerBootstrap {
|
|||||||
.addFile("test.lua", program)
|
.addFile("test.lua", program)
|
||||||
.addFile("startup.lua", "assertion.assert(pcall(loadfile('test.lua', nil, _ENV))) os.shutdown()");
|
.addFile("startup.lua", "assertion.assert(pcall(loadfile('test.lua', nil, _ENV))) os.shutdown()");
|
||||||
|
|
||||||
run(new ReadOnlyWritableMount(mount), setup, maxTimes);
|
run(mount, setup, maxTimes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void run(String program, int maxTimes) {
|
public static void run(String program, int maxTimes) {
|
||||||
|
@@ -0,0 +1,46 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.filesystem;
|
||||||
|
|
||||||
|
import dan200.computercraft.api.filesystem.Mount;
|
||||||
|
import dan200.computercraft.api.filesystem.WritableMount;
|
||||||
|
import dan200.computercraft.test.core.filesystem.MountContract;
|
||||||
|
import dan200.computercraft.test.core.filesystem.WritableMountContract;
|
||||||
|
import org.opentest4j.TestAbortedException;
|
||||||
|
|
||||||
|
public class MemoryMountTest implements MountContract, WritableMountContract {
|
||||||
|
@Override
|
||||||
|
public Mount createSkeleton() {
|
||||||
|
var mount = new MemoryMount();
|
||||||
|
mount.addFile("f.lua", "");
|
||||||
|
mount.addFile("dir/file.lua", "print('testing')", EPOCH, MODIFY_TIME);
|
||||||
|
return mount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MountAccess createMount(long capacity) {
|
||||||
|
var mount = new MemoryMount(capacity);
|
||||||
|
return new MountAccess() {
|
||||||
|
@Override
|
||||||
|
public WritableMount mount() {
|
||||||
|
return mount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void makeReadOnly(String path) {
|
||||||
|
throw new TestAbortedException("Not supported for MemoryMount");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void ensuresExist() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long computeRemainingSpace() {
|
||||||
|
return mount.getRemainingSpace();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@@ -27,7 +27,7 @@ object HttpServer {
|
|||||||
const val URL: String = "http://127.0.0.1:$PORT"
|
const val URL: String = "http://127.0.0.1:$PORT"
|
||||||
const val WS_URL: String = "ws://127.0.0.1:$PORT/ws"
|
const val WS_URL: String = "ws://127.0.0.1:$PORT/ws"
|
||||||
|
|
||||||
fun runServer(run: () -> Unit) {
|
fun runServer(run: (stop: () -> Unit) -> Unit) {
|
||||||
val workerGroup: EventLoopGroup = NioEventLoopGroup(2)
|
val workerGroup: EventLoopGroup = NioEventLoopGroup(2)
|
||||||
try {
|
try {
|
||||||
val ch = ServerBootstrap()
|
val ch = ServerBootstrap()
|
||||||
@@ -48,7 +48,7 @@ object HttpServer {
|
|||||||
},
|
},
|
||||||
).bind(PORT).sync().channel()
|
).bind(PORT).sync().channel()
|
||||||
try {
|
try {
|
||||||
run()
|
run { workerGroup.shutdownGracefully() }
|
||||||
} finally {
|
} finally {
|
||||||
ch.close().sync()
|
ch.close().sync()
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,7 @@
|
|||||||
package dan200.computercraft.core.apis.http
|
package dan200.computercraft.core.apis.http
|
||||||
|
|
||||||
import dan200.computercraft.api.lua.Coerced
|
import dan200.computercraft.api.lua.Coerced
|
||||||
|
import dan200.computercraft.api.lua.LuaException
|
||||||
import dan200.computercraft.api.lua.ObjectArguments
|
import dan200.computercraft.api.lua.ObjectArguments
|
||||||
import dan200.computercraft.core.CoreConfig
|
import dan200.computercraft.core.CoreConfig
|
||||||
import dan200.computercraft.core.apis.HTTPAPI
|
import dan200.computercraft.core.apis.HTTPAPI
|
||||||
@@ -22,7 +23,9 @@ import org.hamcrest.Matchers.*
|
|||||||
import org.junit.jupiter.api.AfterAll
|
import org.junit.jupiter.api.AfterAll
|
||||||
import org.junit.jupiter.api.BeforeAll
|
import org.junit.jupiter.api.BeforeAll
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.assertThrows
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
class TestHttpApi {
|
class TestHttpApi {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -79,12 +82,36 @@ class TestHttpApi {
|
|||||||
|
|
||||||
websocket.close()
|
websocket.close()
|
||||||
|
|
||||||
|
val closeEvent = pullEventOrTimeout(500.milliseconds, "websocket_closed")
|
||||||
|
assertThat("No event was queued", closeEvent, equalTo(null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Queues an event when the socket is externally closed`() {
|
||||||
|
runServer { stop ->
|
||||||
|
LuaTaskRunner.runTest {
|
||||||
|
val httpApi = addApi(HTTPAPI(environment))
|
||||||
|
assertThat("http.websocket succeeded", httpApi.websocket(ObjectArguments(WS_URL)), array(equalTo(true)))
|
||||||
|
|
||||||
|
val connectEvent = pullEvent()
|
||||||
|
assertThat(connectEvent, array(equalTo("websocket_success"), equalTo(WS_URL), isA(WebsocketHandle::class.java)))
|
||||||
|
|
||||||
|
val websocket = connectEvent[2] as WebsocketHandle
|
||||||
|
|
||||||
|
stop()
|
||||||
|
|
||||||
val closeEvent = pullEvent("websocket_closed")
|
val closeEvent = pullEvent("websocket_closed")
|
||||||
assertThat(
|
assertThat(
|
||||||
"Websocket was closed",
|
"Websocket was closed",
|
||||||
closeEvent,
|
closeEvent,
|
||||||
array(equalTo("websocket_closed"), equalTo(WS_URL), equalTo("Connection closed"), equalTo(null)),
|
array(equalTo("websocket_closed"), equalTo(WS_URL), equalTo("Connection closed"), equalTo(null)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assertThrows<LuaException>("Throws an exception when sending") {
|
||||||
|
websocket.send(Coerced("hello"), Optional.of(false))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,10 +10,10 @@ import java.util.Deque;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An {@link AutoCloseable} implementation which can be used to combine other [AutoCloseable] instances.
|
* An {@link AutoCloseable} implementation which can be used to combine other {@link AutoCloseable} instances.
|
||||||
* <p>
|
* <p>
|
||||||
* Values which implement {@link AutoCloseable} can be dynamically registered with [CloseScope.add]. When the scope is
|
* Values which implement {@link AutoCloseable} can be dynamically registered with {@link CloseScope#add(AutoCloseable)}.
|
||||||
* closed, each value is closed in the opposite order.
|
* When the scope is closed, each value is closed in the opposite order.
|
||||||
* <p>
|
* <p>
|
||||||
* This is largely intended for cases where it's not appropriate to nest try-with-resources blocks, for instance when
|
* This is largely intended for cases where it's not appropriate to nest try-with-resources blocks, for instance when
|
||||||
* nested would be too deep or when objects are dynamically created.
|
* nested would be too deep or when objects are dynamically created.
|
||||||
|
@@ -0,0 +1,23 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.test.core;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayNameGeneration;
|
||||||
|
import org.junit.jupiter.api.DisplayNameGenerator;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link DisplayNameGenerator} which replaces underscores with spaces. This is equivalent to
|
||||||
|
* {@link DisplayNameGenerator.ReplaceUnderscores}, but excludes the parameter types.
|
||||||
|
*
|
||||||
|
* @see DisplayNameGeneration
|
||||||
|
*/
|
||||||
|
public class ReplaceUnderscoresDisplayNameGenerator extends DisplayNameGenerator.ReplaceUnderscores {
|
||||||
|
@Override
|
||||||
|
public String generateDisplayNameForMethod(Class<?> testClass, Method testMethod) {
|
||||||
|
return testMethod.getName().replace('_', ' ');
|
||||||
|
}
|
||||||
|
}
|
@@ -11,10 +11,9 @@ import dan200.computercraft.core.computer.ComputerEnvironment;
|
|||||||
import dan200.computercraft.core.computer.GlobalEnvironment;
|
import dan200.computercraft.core.computer.GlobalEnvironment;
|
||||||
import dan200.computercraft.core.filesystem.FileMount;
|
import dan200.computercraft.core.filesystem.FileMount;
|
||||||
import dan200.computercraft.core.filesystem.JarMount;
|
import dan200.computercraft.core.filesystem.JarMount;
|
||||||
|
import dan200.computercraft.core.filesystem.MemoryMount;
|
||||||
import dan200.computercraft.core.metrics.Metric;
|
import dan200.computercraft.core.metrics.Metric;
|
||||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||||
import dan200.computercraft.test.core.filesystem.MemoryMount;
|
|
||||||
import dan200.computercraft.test.core.filesystem.ReadOnlyWritableMount;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -32,7 +31,7 @@ public class BasicEnvironment implements ComputerEnvironment, GlobalEnvironment,
|
|||||||
private final WritableMount mount;
|
private final WritableMount mount;
|
||||||
|
|
||||||
public BasicEnvironment() {
|
public BasicEnvironment() {
|
||||||
this(new ReadOnlyWritableMount(new MemoryMount()));
|
this(new MemoryMount());
|
||||||
}
|
}
|
||||||
|
|
||||||
public BasicEnvironment(WritableMount mount) {
|
public BasicEnvironment(WritableMount mount) {
|
||||||
|
@@ -1,59 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package dan200.computercraft.test.core.filesystem;
|
|
||||||
|
|
||||||
import dan200.computercraft.api.filesystem.FileOperationException;
|
|
||||||
import dan200.computercraft.api.filesystem.Mount;
|
|
||||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
|
||||||
|
|
||||||
import java.nio.channels.SeekableByteChannel;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A read-only mount {@link Mount} which provides a list of in-memory set of files.
|
|
||||||
*/
|
|
||||||
public class MemoryMount implements Mount {
|
|
||||||
private final Map<String, byte[]> files = new HashMap<>();
|
|
||||||
private final Set<String> directories = new HashSet<>();
|
|
||||||
|
|
||||||
public MemoryMount() {
|
|
||||||
directories.add("");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean exists(String path) {
|
|
||||||
return files.containsKey(path) || directories.contains(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isDirectory(String path) {
|
|
||||||
return directories.contains(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void list(String path, List<String> files) {
|
|
||||||
for (var file : this.files.keySet()) {
|
|
||||||
if (file.startsWith(path)) files.add(file.substring(path.length() + 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getSize(String path) {
|
|
||||||
throw new RuntimeException("Not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SeekableByteChannel openForRead(String path) throws FileOperationException {
|
|
||||||
var file = files.get(path);
|
|
||||||
if (file == null) throw new FileOperationException(path, "File not found");
|
|
||||||
return new ArrayByteChannel(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MemoryMount addFile(String file, String contents) {
|
|
||||||
files.put(file, contents.getBytes(StandardCharsets.UTF_8));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -34,7 +34,10 @@ public interface MountContract {
|
|||||||
* Create a skeleton mount. This should contain the following files:
|
* Create a skeleton mount. This should contain the following files:
|
||||||
*
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>{@code dir/file.lua}, containing {@code print('testing')}.</li>
|
* <li>
|
||||||
|
* {@code dir/file.lua}, containing {@code print('testing')}. If {@linkplain #hasFileTimes() file times are
|
||||||
|
* supported}, it should have a modification time of {@link #MODIFY_TIME}.
|
||||||
|
* </li>
|
||||||
* <li>{@code f.lua}, containing nothing.</li>
|
* <li>{@code f.lua}, containing nothing.</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
|
@@ -0,0 +1,33 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.test.core.filesystem;
|
||||||
|
|
||||||
|
import dan200.computercraft.api.filesystem.WritableMount;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.channels.Channels;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility functions for working with mounts.
|
||||||
|
*/
|
||||||
|
public final class Mounts {
|
||||||
|
private Mounts() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a file to this mount.
|
||||||
|
*
|
||||||
|
* @param mount The mount to modify.
|
||||||
|
* @param path The path to write to.
|
||||||
|
* @param contents The contents of this path.
|
||||||
|
* @throws IOException If writing fails.
|
||||||
|
*/
|
||||||
|
public static void writeFile(WritableMount mount, String path, String contents) throws IOException {
|
||||||
|
try (var handle = Channels.newWriter(mount.openForWrite(path), StandardCharsets.UTF_8)) {
|
||||||
|
handle.write(contents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,91 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package dan200.computercraft.test.core.filesystem;
|
|
||||||
|
|
||||||
import dan200.computercraft.api.filesystem.FileOperationException;
|
|
||||||
import dan200.computercraft.api.filesystem.Mount;
|
|
||||||
import dan200.computercraft.api.filesystem.WritableMount;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.channels.SeekableByteChannel;
|
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps a {@link Mount} into a read-only {@link WritableMount}.
|
|
||||||
*
|
|
||||||
* @param mount The original read-only mount we're wrapping.
|
|
||||||
*/
|
|
||||||
public record ReadOnlyWritableMount(Mount mount) implements WritableMount {
|
|
||||||
@Override
|
|
||||||
public boolean exists(String path) throws IOException {
|
|
||||||
return mount.exists(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isDirectory(String path) throws IOException {
|
|
||||||
return mount.isDirectory(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void list(String path, List<String> contents) throws IOException {
|
|
||||||
mount.list(path, contents);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getSize(String path) throws IOException {
|
|
||||||
return mount.getSize(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SeekableByteChannel openForRead(String path) throws IOException {
|
|
||||||
return mount.openForRead(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public BasicFileAttributes getAttributes(String path) throws IOException {
|
|
||||||
return mount.getAttributes(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void makeDirectory(String path) throws IOException {
|
|
||||||
throw new FileOperationException(path, "Access denied");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(String path) throws IOException {
|
|
||||||
throw new FileOperationException(path, "Access denied");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void rename(String source, String dest) throws IOException {
|
|
||||||
throw new FileOperationException(source, "Access denied");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SeekableByteChannel openForWrite(String path) throws IOException {
|
|
||||||
throw new FileOperationException(path, "Access denied");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SeekableByteChannel openForAppend(String path) throws IOException {
|
|
||||||
throw new FileOperationException(path, "Access denied");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getRemainingSpace() {
|
|
||||||
return Integer.MAX_VALUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getCapacity() {
|
|
||||||
return Integer.MAX_VALUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isReadOnly(String path) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -5,10 +5,16 @@
|
|||||||
package dan200.computercraft.test.core.filesystem;
|
package dan200.computercraft.test.core.filesystem;
|
||||||
|
|
||||||
import dan200.computercraft.api.filesystem.WritableMount;
|
import dan200.computercraft.api.filesystem.WritableMount;
|
||||||
|
import dan200.computercraft.api.lua.LuaValues;
|
||||||
|
import dan200.computercraft.test.core.ReplaceUnderscoresDisplayNameGenerator;
|
||||||
|
import org.junit.jupiter.api.DisplayNameGeneration;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
import org.opentest4j.TestAbortedException;
|
import org.opentest4j.TestAbortedException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
@@ -17,9 +23,16 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
*
|
*
|
||||||
* @see MountContract
|
* @see MountContract
|
||||||
*/
|
*/
|
||||||
|
@DisplayNameGeneration(ReplaceUnderscoresDisplayNameGenerator.class)
|
||||||
public interface WritableMountContract {
|
public interface WritableMountContract {
|
||||||
long CAPACITY = 1_000_000;
|
long CAPACITY = 1_000_000;
|
||||||
|
|
||||||
|
String LONG_CONTENTS = "This is some example text.\n".repeat(100);
|
||||||
|
|
||||||
|
static Stream<String> fileContents() {
|
||||||
|
return Stream.of("", LONG_CONTENTS);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new empty mount.
|
* Create a new empty mount.
|
||||||
*
|
*
|
||||||
@@ -43,18 +56,30 @@ public interface WritableMountContract {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
default void testRootWritable() throws IOException {
|
default void Root_is_writable() throws IOException {
|
||||||
assertFalse(createExisting(CAPACITY).mount().isReadOnly("/"));
|
assertFalse(createExisting(CAPACITY).mount().isReadOnly("/"));
|
||||||
assertFalse(createMount(CAPACITY).mount().isReadOnly("/"));
|
assertFalse(createMount(CAPACITY).mount().isReadOnly("/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
default void testMissingDirWritable() throws IOException {
|
default void Missing_dir_is_writable() throws IOException {
|
||||||
assertFalse(createExisting(CAPACITY).mount().isReadOnly("/foo/bar/baz/qux"));
|
assertFalse(createExisting(CAPACITY).mount().isReadOnly("/foo/bar/baz/qux"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
default void testDirReadOnly() throws IOException {
|
default void Make_directory_recursive() throws IOException {
|
||||||
|
var access = createMount(CAPACITY);
|
||||||
|
var mount = access.mount();
|
||||||
|
mount.makeDirectory("a/b/c");
|
||||||
|
|
||||||
|
assertTrue(mount.isDirectory("a/b/c"));
|
||||||
|
|
||||||
|
assertEquals(CAPACITY - 500 * 3, mount.getRemainingSpace());
|
||||||
|
assertEquals(access.computeRemainingSpace(), access.mount().getRemainingSpace(), "Free space is inconsistent");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
default void Can_make_read_only() throws IOException {
|
||||||
var root = createMount(CAPACITY);
|
var root = createMount(CAPACITY);
|
||||||
var mount = root.mount();
|
var mount = root.mount();
|
||||||
mount.makeDirectory("read-only");
|
mount.makeDirectory("read-only");
|
||||||
@@ -66,18 +91,97 @@ public interface WritableMountContract {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
default void testMovePreservesSpace() throws IOException {
|
default void Initial_free_space_and_capacity() throws IOException {
|
||||||
|
var mount = createExisting(CAPACITY).mount();
|
||||||
|
assertEquals(CAPACITY, mount.getCapacity());
|
||||||
|
assertEquals(CAPACITY, mount.getRemainingSpace());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
default void Write_updates_size_and_free_space() throws IOException {
|
||||||
var access = createExisting(CAPACITY);
|
var access = createExisting(CAPACITY);
|
||||||
var mount = access.mount();
|
var mount = access.mount();
|
||||||
mount.openForWrite("foo").close();
|
|
||||||
|
Mounts.writeFile(mount, "hello.txt", LONG_CONTENTS);
|
||||||
|
assertEquals(LONG_CONTENTS.length(), mount.getSize("hello.txt"));
|
||||||
|
assertEquals(CAPACITY - LONG_CONTENTS.length(), mount.getRemainingSpace());
|
||||||
|
|
||||||
|
Mounts.writeFile(mount, "hello.txt", "");
|
||||||
|
assertEquals(0, mount.getSize("hello.txt"));
|
||||||
|
assertEquals(CAPACITY - 500, mount.getRemainingSpace());
|
||||||
|
assertEquals(access.computeRemainingSpace(), access.mount().getRemainingSpace(), "Free space is inconsistent");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
default void Append_jumps_to_file_end() throws IOException {
|
||||||
|
var access = createExisting(CAPACITY);
|
||||||
|
var mount = access.mount();
|
||||||
|
|
||||||
|
Mounts.writeFile(mount, "a.txt", "example");
|
||||||
|
|
||||||
|
try (var handle = mount.openForAppend("a.txt")) {
|
||||||
|
assertEquals(7, handle.position());
|
||||||
|
handle.write(LuaValues.encode(" text"));
|
||||||
|
assertEquals(12, handle.position());
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(12, mount.getSize("a.txt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "\"{0}\"")
|
||||||
|
@MethodSource("fileContents")
|
||||||
|
default void Move_file(String contents) throws IOException {
|
||||||
|
var access = createExisting(CAPACITY);
|
||||||
|
var mount = access.mount();
|
||||||
|
Mounts.writeFile(mount, "src.txt", contents);
|
||||||
|
|
||||||
var remainingSpace = mount.getRemainingSpace();
|
var remainingSpace = mount.getRemainingSpace();
|
||||||
mount.rename("foo", "bar");
|
|
||||||
|
|
||||||
|
mount.rename("src.txt", "dest.txt");
|
||||||
|
assertFalse(mount.exists("src.txt"));
|
||||||
|
assertTrue(mount.exists("dest.txt"));
|
||||||
|
|
||||||
|
assertEquals(contents.length(), mount.getSize("dest.txt"));
|
||||||
assertEquals(remainingSpace, mount.getRemainingSpace(), "Free space has changed after moving");
|
assertEquals(remainingSpace, mount.getRemainingSpace(), "Free space has changed after moving");
|
||||||
assertEquals(access.computeRemainingSpace(), access.mount().getRemainingSpace(), "Free space is inconsistent");
|
assertEquals(access.computeRemainingSpace(), access.mount().getRemainingSpace(), "Free space is inconsistent");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "\"{0}\"")
|
||||||
|
@MethodSource("fileContents")
|
||||||
|
default void Move_file_fails_when_destination_exists(String contents) throws IOException {
|
||||||
|
var access = createExisting(CAPACITY);
|
||||||
|
var mount = access.mount();
|
||||||
|
Mounts.writeFile(mount, "src.txt", contents);
|
||||||
|
Mounts.writeFile(mount, "dest.txt", "dest");
|
||||||
|
|
||||||
|
var remainingSpace = mount.getRemainingSpace();
|
||||||
|
|
||||||
|
assertThrows(IOException.class, () -> mount.rename("src.txt", "dest.txt"));
|
||||||
|
|
||||||
|
assertEquals(contents.length(), mount.getSize("src.txt"));
|
||||||
|
assertEquals(4, mount.getSize("dest.txt"));
|
||||||
|
|
||||||
|
assertEquals(remainingSpace, mount.getRemainingSpace(), "Free space has changed despite no move occurred.");
|
||||||
|
assertEquals(access.computeRemainingSpace(), access.mount().getRemainingSpace(), "Free space is inconsistent");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
default void Move_file_fails_when_source_does_not_exist() throws IOException {
|
||||||
|
var access = createExisting(CAPACITY);
|
||||||
|
var mount = access.mount();
|
||||||
|
Mounts.writeFile(mount, "dest.txt", "dest");
|
||||||
|
|
||||||
|
var remainingSpace = mount.getRemainingSpace();
|
||||||
|
|
||||||
|
assertThrows(IOException.class, () -> mount.rename("src.txt", "dest.txt"));
|
||||||
|
|
||||||
|
assertFalse(mount.exists("src.txt"));
|
||||||
|
assertEquals(4, mount.getSize("dest.txt"));
|
||||||
|
|
||||||
|
assertEquals(remainingSpace, mount.getRemainingSpace(), "Free space has changed despite no move occurred.");
|
||||||
|
assertEquals(access.computeRemainingSpace(), access.mount().getRemainingSpace(), "Free space is inconsistent");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps a {@link WritableMount} with additional operations.
|
* Wraps a {@link WritableMount} with additional operations.
|
||||||
*/
|
*/
|
||||||
@@ -90,7 +194,7 @@ public interface WritableMountContract {
|
|||||||
WritableMount mount();
|
WritableMount mount();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make a path read-only. This may throw a {@link TestAbortedException} if
|
* Make a path read-only. This may throw a {@link TestAbortedException} if this operation is not supported.
|
||||||
*
|
*
|
||||||
* @param path The mount-relative path.
|
* @param path The mount-relative path.
|
||||||
*/
|
*/
|
||||||
|
@@ -12,6 +12,7 @@ import dan200.computercraft.core.apis.OSAPI
|
|||||||
import dan200.computercraft.core.apis.PeripheralAPI
|
import dan200.computercraft.core.apis.PeripheralAPI
|
||||||
import kotlinx.coroutines.CancellableContinuation
|
import kotlinx.coroutines.CancellableContinuation
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,6 +30,9 @@ interface LuaTaskContext {
|
|||||||
/** Pull a Lua event */
|
/** Pull a Lua event */
|
||||||
suspend fun pullEvent(event: String? = null): Array<out Any?>
|
suspend fun pullEvent(event: String? = null): Array<out Any?>
|
||||||
|
|
||||||
|
suspend fun pullEventOrTimeout(timeout: Duration, event: String? = null): Array<out Any?>? =
|
||||||
|
withTimeoutOrNull(timeout) { pullEvent(event) }
|
||||||
|
|
||||||
/** Resolve a [MethodResult] until completion, returning the resulting values. */
|
/** Resolve a [MethodResult] until completion, returning the resulting values. */
|
||||||
suspend fun MethodResult.await(): Array<out Any?>? {
|
suspend fun MethodResult.await(): Array<out Any?>? {
|
||||||
var result = this
|
var result = this
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"type": "computercraft:computer_upgrade",
|
"type": "computercraft:computer_upgrade",
|
||||||
"category": "redstone",
|
"category": "redstone",
|
||||||
"family": "ADVANCED",
|
"family": "advanced",
|
||||||
"key": {"#": {"tag": "c:gold_ingots"}, "C": {"item": "computercraft:computer_normal"}},
|
"key": {"#": {"tag": "c:gold_ingots"}, "C": {"item": "computercraft:computer_normal"}},
|
||||||
"pattern": ["###", "#C#", "# #"],
|
"pattern": ["###", "#C#", "# #"],
|
||||||
"result": {"item": "computercraft:computer_advanced"},
|
"result": {"item": "computercraft:computer_advanced"},
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"type": "computercraft:computer_upgrade",
|
"type": "computercraft:computer_upgrade",
|
||||||
"category": "redstone",
|
"category": "redstone",
|
||||||
"family": "ADVANCED",
|
"family": "advanced",
|
||||||
"key": {"#": {"tag": "c:gold_ingots"}, "C": {"item": "computercraft:pocket_computer_normal"}},
|
"key": {"#": {"tag": "c:gold_ingots"}, "C": {"item": "computercraft:pocket_computer_normal"}},
|
||||||
"pattern": ["###", "#C#", "# #"],
|
"pattern": ["###", "#C#", "# #"],
|
||||||
"result": {"item": "computercraft:pocket_computer_advanced"},
|
"result": {"item": "computercraft:pocket_computer_advanced"},
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"type": "minecraft:crafting_shapeless",
|
"type": "computercraft:shapeless",
|
||||||
"category": "misc",
|
"category": "misc",
|
||||||
"ingredients": [{"tag": "c:skulls"}, {"item": "computercraft:monitor_normal"}],
|
"ingredients": [{"tag": "c:skulls"}, {"item": "computercraft:monitor_normal"}],
|
||||||
"result": {
|
"result": {
|
||||||
"item": "minecraft:player_head",
|
"item": "minecraft:player_head",
|
||||||
"nbt": "{SkullOwner:{Id:\"6d074736-b1e9-4378-a99b-bd8777821c9c\",Name:\"Cloudhunter\"}}"
|
"nbt": "{SkullOwner:{Id:[I;1829193526,-1310112904,-1449411193,2005015708],Name:\"Cloudhunter\"}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"type": "minecraft:crafting_shapeless",
|
"type": "computercraft:shapeless",
|
||||||
"category": "misc",
|
"category": "misc",
|
||||||
"ingredients": [{"tag": "c:skulls"}, {"item": "computercraft:computer_advanced"}],
|
"ingredients": [{"tag": "c:skulls"}, {"item": "computercraft:computer_advanced"}],
|
||||||
"result": {
|
"result": {
|
||||||
"item": "minecraft:player_head",
|
"item": "minecraft:player_head",
|
||||||
"nbt": "{SkullOwner:{Id:\"f3c8d69b-0776-4512-8434-d1b2165909eb\",Name:\"dan200\"}}"
|
"nbt": "{SkullOwner:{Id:[I;-204941669,125191442,-2076913230,374933995],Name:\"dan200\"}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"type": "computercraft:turtle",
|
"type": "computercraft:turtle",
|
||||||
"category": "redstone",
|
"category": "redstone",
|
||||||
"family": "ADVANCED",
|
"family": "advanced",
|
||||||
"key": {
|
"key": {
|
||||||
"#": {"tag": "c:gold_ingots"},
|
"#": {"tag": "c:gold_ingots"},
|
||||||
"C": {"item": "computercraft:computer_advanced"},
|
"C": {"item": "computercraft:computer_advanced"},
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"type": "computercraft:computer_upgrade",
|
"type": "computercraft:computer_upgrade",
|
||||||
"category": "redstone",
|
"category": "redstone",
|
||||||
"family": "ADVANCED",
|
"family": "advanced",
|
||||||
"key": {
|
"key": {
|
||||||
"#": {"tag": "c:gold_ingots"},
|
"#": {"tag": "c:gold_ingots"},
|
||||||
"B": {"item": "minecraft:gold_block"},
|
"B": {"item": "minecraft:gold_block"},
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"type": "computercraft:turtle",
|
"type": "computercraft:turtle",
|
||||||
"category": "redstone",
|
"category": "redstone",
|
||||||
"family": "NORMAL",
|
"family": "normal",
|
||||||
"key": {
|
"key": {
|
||||||
"#": {"tag": "c:iron_ingots"},
|
"#": {"tag": "c:iron_ingots"},
|
||||||
"C": {"item": "computercraft:computer_normal"},
|
"C": {"item": "computercraft:computer_normal"},
|
||||||
|
@@ -0,0 +1 @@
|
|||||||
|
{"replace": false, "values": ["#computercraft:wired_modem"]}
|
@@ -1,25 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package dan200.computercraft.mixin;
|
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import dan200.computercraft.shared.FabricCommonHooks;
|
|
||||||
import net.minecraft.world.item.ItemStack;
|
|
||||||
import net.minecraft.world.item.crafting.ShapedRecipe;
|
|
||||||
import org.spongepowered.asm.mixin.Mixin;
|
|
||||||
import org.spongepowered.asm.mixin.injection.At;
|
|
||||||
import org.spongepowered.asm.mixin.injection.Inject;
|
|
||||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
|
||||||
|
|
||||||
@Mixin(ShapedRecipe.class)
|
|
||||||
class ShapedRecipeMixin {
|
|
||||||
@Inject(method = "itemStackFromJson", at = @At("RETURN"))
|
|
||||||
@SuppressWarnings("UnusedMethod")
|
|
||||||
private static void itemStackFromJson(JsonObject json, CallbackInfoReturnable<ItemStack> cir) {
|
|
||||||
// This is a fairly invasive mixin in the sense that every mod goes through this code path. We might want to
|
|
||||||
// remove this and use custom recipes types in the future.
|
|
||||||
FabricCommonHooks.addRecipeResultTag(cir.getReturnValue(), json);
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user