mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-10-22 09:27:39 +00:00
Compare commits
24 Commits
v1.20.1-1.
...
v1.20.1-1.
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bcb3e9bd53 | ||
![]() |
c30bffbd0f | ||
![]() |
91c41856c5 | ||
![]() |
18c9723308 | ||
![]() |
aee382ed70 | ||
![]() |
6656da5877 | ||
![]() |
09e521727f | ||
![]() |
cab66a2d6e | ||
![]() |
8eabd4f303 | ||
![]() |
e3ced84885 | ||
![]() |
0929ab577d | ||
![]() |
2228733abc | ||
![]() |
e67c94d1bd | ||
![]() |
ae5a661a47 | ||
![]() |
0ff58cdc3e | ||
![]() |
1747c74770 | ||
![]() |
71669cf49c | ||
![]() |
bd327e37eb | ||
![]() |
bdce9a8170 | ||
![]() |
7e5598d084 | ||
![]() |
440fca6535 | ||
![]() |
6635edd35c | ||
![]() |
93ad40efbb | ||
![]() |
27dc8b5b2c |
15
.reuse/dep5
15
.reuse/dep5
@@ -53,6 +53,7 @@ Files:
|
||||
projects/common/src/main/resources/assets/computercraft/textures/*
|
||||
projects/common/src/main/resources/pack.mcmeta
|
||||
projects/common/src/main/resources/pack.png
|
||||
projects/core/src/main/resources/assets/computercraft/textures/gui/term_font.png
|
||||
projects/core/src/main/resources/data/computercraft/lua/rom/autorun/.ignoreme
|
||||
projects/core/src/main/resources/data/computercraft/lua/rom/help/*
|
||||
projects/core/src/main/resources/data/computercraft/lua/rom/programs/fun/advanced/levels/*
|
||||
@@ -62,11 +63,23 @@ Copyright: 2011 Daniel Ratcliffe
|
||||
License: LicenseRef-CCPL
|
||||
|
||||
Files:
|
||||
projects/common/src/main/resources/assets/computercraft/lang/*
|
||||
projects/common/src/main/resources/assets/computercraft/lang/cs_cz.json
|
||||
projects/common/src/main/resources/assets/computercraft/lang/ko_kr.json
|
||||
projects/common/src/main/resources/assets/computercraft/lang/pl_pl.json
|
||||
projects/common/src/main/resources/assets/computercraft/lang/pt_br.json
|
||||
projects/common/src/main/resources/assets/computercraft/lang/ru_ru.json
|
||||
projects/common/src/main/resources/assets/computercraft/lang/uk_ua.json
|
||||
projects/common/src/main/resources/assets/computercraft/lang/zh_cn.json
|
||||
Comment: Community-contributed license files
|
||||
Copyright: 2017 The CC: Tweaked Developers
|
||||
License: LicenseRef-CCPL
|
||||
|
||||
Files:
|
||||
projects/common/src/main/resources/assets/computercraft/lang/*
|
||||
Comment: Community-contributed license files
|
||||
Copyright: 2017 The CC: Tweaked Developers
|
||||
License: MPL-2.0
|
||||
|
||||
Files:
|
||||
.github/*
|
||||
Comment:
|
||||
|
@@ -2,7 +2,11 @@
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
import cc.tweaked.gradle.JUnitExt
|
||||
import net.fabricmc.loom.api.LoomGradleExtensionAPI
|
||||
import net.fabricmc.loom.util.gradle.SourceSetHelper
|
||||
import org.jetbrains.gradle.ext.compiler
|
||||
import org.jetbrains.gradle.ext.runConfigurations
|
||||
import org.jetbrains.gradle.ext.settings
|
||||
|
||||
plugins {
|
||||
@@ -38,6 +42,50 @@ githubRelease {
|
||||
|
||||
tasks.publish { dependsOn(tasks.githubRelease) }
|
||||
|
||||
idea.project.settings.runConfigurations {
|
||||
register<JUnitExt>("Core Tests") {
|
||||
vmParameters = "-ea"
|
||||
moduleName = "${idea.project.name}.core.test"
|
||||
packageName = ""
|
||||
}
|
||||
|
||||
register<JUnitExt>("CraftOS Tests") {
|
||||
vmParameters = "-ea"
|
||||
moduleName = "${idea.project.name}.core.test"
|
||||
className = "dan200.computercraft.core.ComputerTestDelegate"
|
||||
}
|
||||
|
||||
register<JUnitExt>("CraftOS Tests (Fast)") {
|
||||
vmParameters = "-ea -Dcc.skip_keywords=slow"
|
||||
moduleName = "${idea.project.name}.core.test"
|
||||
className = "dan200.computercraft.core.ComputerTestDelegate"
|
||||
}
|
||||
|
||||
register<JUnitExt>("Common Tests") {
|
||||
vmParameters = "-ea"
|
||||
moduleName = "${idea.project.name}.common.test"
|
||||
packageName = ""
|
||||
}
|
||||
|
||||
register<JUnitExt>("Fabric Tests") {
|
||||
val fabricProject = evaluationDependsOn(":fabric")
|
||||
val classPathGroup = fabricProject.extensions.getByType<LoomGradleExtensionAPI>().mods
|
||||
.joinToString(File.pathSeparator + File.pathSeparator) { modSettings ->
|
||||
SourceSetHelper.getClasspath(modSettings, project).joinToString(File.pathSeparator) { it.absolutePath }
|
||||
}
|
||||
|
||||
vmParameters = "-ea -Dfabric.classPathGroups=$classPathGroup"
|
||||
moduleName = "${idea.project.name}.fabric.test"
|
||||
packageName = ""
|
||||
}
|
||||
|
||||
register<JUnitExt>("Forge Tests") {
|
||||
vmParameters = "-ea"
|
||||
moduleName = "${idea.project.name}.forge.test"
|
||||
packageName = ""
|
||||
}
|
||||
}
|
||||
|
||||
idea.project.settings.compiler.javac {
|
||||
// We want ErrorProne to be present when compiling via IntelliJ, as it offers some helpful warnings
|
||||
// and errors. Loop through our source sets and find the appropriate flags.
|
||||
|
@@ -50,10 +50,11 @@ dependencies {
|
||||
implementation(libs.curseForgeGradle)
|
||||
implementation(libs.fabric.loom)
|
||||
implementation(libs.forgeGradle)
|
||||
implementation(libs.ideaExt)
|
||||
implementation(libs.librarian)
|
||||
implementation(libs.minotaur)
|
||||
implementation(libs.vineflower)
|
||||
implementation(libs.vanillaGradle)
|
||||
implementation(libs.vineflower)
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
|
@@ -173,7 +173,7 @@ abstract class CCTweakedExtension(
|
||||
}
|
||||
|
||||
fun <T> jacoco(task: NamedDomainObjectProvider<T>) where T : Task, T : JavaForkOptions {
|
||||
val classDump = project.buildDir.resolve("jacocoClassDump/${task.name}")
|
||||
val classDump = project.layout.buildDirectory.dir("jacocoClassDump/${task.name}")
|
||||
val reportTaskName = "jacoco${task.name.capitalized()}Report"
|
||||
|
||||
val jacoco = project.extensions.getByType(JacocoPluginExtension::class.java)
|
||||
@@ -185,7 +185,7 @@ abstract class CCTweakedExtension(
|
||||
jacoco.applyTo(this)
|
||||
extensions.configure(JacocoTaskExtension::class.java) {
|
||||
includes = listOf("dan200.computercraft.*")
|
||||
classDumpDir = classDump
|
||||
classDumpDir = classDump.get().asFile
|
||||
|
||||
// Older versions of modlauncher don't include a protection domain (and thus no code
|
||||
// source). Jacoco skips such classes by default, so we need to explicitly include them.
|
||||
|
@@ -9,6 +9,10 @@ import org.gradle.api.Project
|
||||
import org.gradle.api.plugins.JavaPlugin
|
||||
import org.gradle.api.plugins.JavaPluginExtension
|
||||
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||
import org.gradle.plugins.ide.idea.model.IdeaModel
|
||||
import org.jetbrains.gradle.ext.IdeaExtPlugin
|
||||
import org.jetbrains.gradle.ext.runConfigurations
|
||||
import org.jetbrains.gradle.ext.settings
|
||||
|
||||
/**
|
||||
* Configures projects to match a shared configuration.
|
||||
@@ -21,6 +25,20 @@ class CCTweakedPlugin : Plugin<Project> {
|
||||
val sourceSets = project.extensions.getByType(JavaPluginExtension::class.java).sourceSets
|
||||
cct.sourceDirectories.add(SourceSetReference.internal(sourceSets.getByName("main")))
|
||||
}
|
||||
|
||||
project.plugins.withType(IdeaExtPlugin::class.java) { extendIdea(project) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the [IdeaExtPlugin] plugin's `runConfiguration` container to also support [JUnitExt].
|
||||
*/
|
||||
private fun extendIdea(project: Project) {
|
||||
val ideaModel = project.extensions.findByName("idea") as IdeaModel? ?: return
|
||||
val ideaProject = ideaModel.project ?: return
|
||||
|
||||
ideaProject.settings.runConfigurations {
|
||||
registerFactory(JUnitExt::class.java) { name -> project.objects.newInstance(JUnitExt::class.java, name) }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
23
buildSrc/src/main/kotlin/cc/tweaked/gradle/IdeaExt.kt
Normal file
23
buildSrc/src/main/kotlin/cc/tweaked/gradle/IdeaExt.kt
Normal file
@@ -0,0 +1,23 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.gradle
|
||||
|
||||
import org.jetbrains.gradle.ext.JUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* A version of [JUnit] with a functional [className].
|
||||
*
|
||||
* See [#92](https://github.com/JetBrains/gradle-idea-ext-plugin/issues/92).
|
||||
*/
|
||||
open class JUnitExt @Inject constructor(nameParam: String) : JUnit(nameParam) {
|
||||
override fun toMap(): MutableMap<String, *> {
|
||||
val map = HashMap(super.toMap())
|
||||
// Should be "class" instead of "className".
|
||||
// See https://github.com/JetBrains/intellij-community/blob/9ba394021dc73a3926f13d6d6cdf434f9ee7046d/plugins/junit/src/com/intellij/execution/junit/JUnitRunConfigurationImporter.kt#L39
|
||||
map["class"] = className
|
||||
return map
|
||||
}
|
||||
}
|
@@ -58,7 +58,7 @@ abstract class ClientJavaExec : JavaExec() {
|
||||
if (!clientDebug) systemProperty("cctest.client", "")
|
||||
if (renderdoc) environment("LD_PRELOAD", "/usr/lib/librenderdoc.so")
|
||||
systemProperty("cctest.gametest-report", testResults.get().asFile.absoluteFile)
|
||||
workingDir(project.buildDir.resolve("gametest").resolve(name))
|
||||
workingDir(project.layout.buildDirectory.dir("gametest/$name"))
|
||||
}
|
||||
|
||||
init {
|
||||
|
@@ -112,7 +112,9 @@ SPDX-License-Identifier: MPL-2.0
|
||||
<module name="LambdaParameterName" />
|
||||
<module name="LocalFinalVariableName" />
|
||||
<module name="LocalVariableName" />
|
||||
<module name="MemberName" />
|
||||
<module name="MemberName">
|
||||
<property name="format" value="^\$?[a-z][a-zA-Z0-9]*$" />
|
||||
</module>
|
||||
<module name="MethodName">
|
||||
<property name="format" value="^(computercraft\$)?[a-z][a-zA-Z0-9]*$" />
|
||||
</module>
|
||||
@@ -122,7 +124,7 @@ SPDX-License-Identifier: MPL-2.0
|
||||
</module>
|
||||
<module name="ParameterName" />
|
||||
<module name="StaticVariableName">
|
||||
<property name="format" value="^[a-z][a-zA-Z0-9]*|CAPABILITY(_[A-Z_]+)?$" />
|
||||
<property name="format" value="^[a-z][a-zA-Z0-9]*$" />
|
||||
</module>
|
||||
<module name="TypeName" />
|
||||
|
||||
|
@@ -6,7 +6,7 @@ see: key To listen to any key press.
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
|
||||
SPDX-License-Identifier: LicenseRef-CCPL
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The [`char`] event is fired when a character is typed on the keyboard.
|
||||
|
@@ -5,7 +5,7 @@ module: [kind=event] key
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
|
||||
SPDX-License-Identifier: LicenseRef-CCPL
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
This event is fired when any key is pressed while the terminal is focused.
|
||||
|
@@ -6,7 +6,7 @@ see: keys For a lookup table of the given keys.
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
|
||||
SPDX-License-Identifier: LicenseRef-CCPL
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
Fired whenever a key is released (or the terminal is closed while a key was being pressed).
|
||||
|
@@ -5,7 +5,7 @@ module: [kind=event] mouse_click
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
|
||||
SPDX-License-Identifier: LicenseRef-CCPL
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
This event is fired when the terminal is clicked with a mouse. This event is only fired on advanced computers (including
|
||||
|
@@ -6,7 +6,7 @@ see: mouse_click For when a mouse button is initially pressed.
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
|
||||
SPDX-License-Identifier: LicenseRef-CCPL
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
This event is fired every time the mouse is moved while a mouse button is being held.
|
||||
|
@@ -5,7 +5,7 @@ module: [kind=event] mouse_scroll
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
|
||||
SPDX-License-Identifier: LicenseRef-CCPL
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
This event is fired when a mouse wheel is scrolled in the terminal.
|
||||
|
@@ -5,7 +5,7 @@ module: [kind=event] mouse_up
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
|
||||
SPDX-License-Identifier: LicenseRef-CCPL
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
This event is fired when a mouse button is released or a held mouse leaves the computer's terminal.
|
||||
|
67
doc/reference/breaking_changes.md
Normal file
67
doc/reference/breaking_changes.md
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
module: [kind=reference] breaking_changes
|
||||
---
|
||||
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
|
||||
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
# Incompatibilities between versions
|
||||
|
||||
CC: Tweaked tries to remain as compatible between versions as possible, meaning most programs written for older version
|
||||
of the mod should run fine on later versions.
|
||||
|
||||
> [External peripherals][!WARNING]
|
||||
>
|
||||
> While CC: Tweaked is relatively stable across versions, this may not be true for other mods which add their own
|
||||
> peripherals. Older programs which interact with external blocks may not work on newer versions of the game.
|
||||
|
||||
However, some changes to the underlying game, or CC: Tweaked's own internals may break some programs. This page serves
|
||||
as documentation for breaking changes and "gotchas" one should look out for between versions.
|
||||
|
||||
## Minecraft 1.13 {#mc-1.13}
|
||||
- The "key code" for [`key`] and [`key_up`] events has changed, due to Minecraft updating to LWJGL 3. Make sure you're
|
||||
using the constants provided by the [`keys`] API, rather than hard-coding numerical values.
|
||||
|
||||
Related to this change, the numpad enter key now has a different key code to the enter key. You may need to adjust
|
||||
your programs to handle both. (Note, the `keys.numpadEnter` constant was defined in pre-1.13 versions of CC, but the
|
||||
`keys.enter` constant was queued when the key was pressed)
|
||||
|
||||
- Minecraft 1.13 removed the concept of item damage and block metadata (see ["The Flattening"][flattening]). As a
|
||||
result `turtle.inspect` no longer provides block metadata, and `turtle.getItemDetail` no longer provides damage.
|
||||
|
||||
- Block states (`turtle.inspect().state`) should provide all the same information as block metadata, but in a much
|
||||
more understandable format.
|
||||
|
||||
- Item and block names now represent a unique item type. For instance, wool is split into 16 separate items
|
||||
(`minecraft:white_wool`, etc...) rather than a single `minecraft:wool` with each meta/damage value specifying the
|
||||
colour.
|
||||
|
||||
- Custom ROMs are now provided using data packs rather than resource packs. This should mostly be a matter of renaming
|
||||
the "assets" folder to "data", and placing it in "datapacks", but there are a couple of other gotchas to look out
|
||||
for:
|
||||
|
||||
- Data packs [impose some restrictions on file names][legal_data_pack]. As a result, your programs and directories
|
||||
must all be lower case.
|
||||
- Due to how data packs are read by CC: Tweaked, you may need to use the `/reload` command to see changes to your
|
||||
pack show up on the computer.
|
||||
|
||||
See [the example datapack][datapack-example] for how to get started.
|
||||
|
||||
- Turtles can now be waterlogged and move "through" water sources rather than breaking them.
|
||||
|
||||
## CC: Tweaked 1.88.0 {#cc-1.88}
|
||||
- Unlabelled computers and turtles now keep their ID when broken, meaning that unlabelled computers/items do not stack.
|
||||
|
||||
## ComputerCraft 1.80pr1 {#cc-1.80}
|
||||
- Programs run via `shell.run` are now started in their own isolated environment. This means globals set by programs
|
||||
will not be accessible outside of this program.
|
||||
|
||||
- Programs containing `/` are looked up in the current directory and are no longer looked up on the path. For instance,
|
||||
you can no longer type `turtle/excavate` to run `/rom/programs/turtle/excavate.lua`.
|
||||
|
||||
[flattening]: https://minecraft.wiki.com/w/Java_Edition_1.13/Flattening
|
||||
[legal_data_pack]: https://minecraft.gamepedia.com/Tutorials/Creating_a_data_pack#Legal_characters
|
||||
[datapack-example]: https://github.com/cc-tweaked/datapack-example "An example datapack for CC: Tweaked"
|
@@ -10,7 +10,7 @@ kotlin.jvm.target.validation.mode=error
|
||||
|
||||
# Mod properties
|
||||
isUnstable=false
|
||||
modVersion=1.108.3
|
||||
modVersion=1.108.4
|
||||
|
||||
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
|
||||
mcVersion=1.20.1
|
||||
|
@@ -21,6 +21,7 @@ autoService = "1.1.1"
|
||||
checkerFramework = "3.32.0"
|
||||
cobalt = "0.7.3"
|
||||
cobalt-next = "0.7.4" # Not a real version, used to constrain the version we accept.
|
||||
commonsCli = "1.3.1"
|
||||
fastutil = "8.5.9"
|
||||
guava = "31.1-jre"
|
||||
jetbrainsAnnotations = "24.0.1"
|
||||
@@ -45,7 +46,6 @@ rubidium = "0.6.1"
|
||||
sodium = "mc1.20-0.4.10"
|
||||
|
||||
# Testing
|
||||
byteBuddy = "1.14.7"
|
||||
hamcrest = "2.2"
|
||||
jqwik = "1.7.4"
|
||||
junit = "5.10.0"
|
||||
@@ -62,14 +62,15 @@ githubRelease = "2.4.1"
|
||||
ideaExt = "1.1.7"
|
||||
illuaminate = "0.1.0-44-g9ee0055"
|
||||
librarian = "1.+"
|
||||
lwjgl = "3.3.1"
|
||||
minotaur = "2.+"
|
||||
mixinGradle = "0.7.+"
|
||||
nullAway = "0.9.9"
|
||||
spotless = "6.21.0"
|
||||
taskTree = "2.1.1"
|
||||
teavm = "0.9.0-SQUID.1"
|
||||
vanillaGradle = "0.2.1-SNAPSHOT"
|
||||
vineflower = "1.11.0"
|
||||
teavm = "0.9.0-SQUID.1"
|
||||
|
||||
[libraries]
|
||||
# Normal dependencies
|
||||
@@ -78,6 +79,7 @@ asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" }
|
||||
autoService = { module = "com.google.auto.service:auto-service", version.ref = "autoService" }
|
||||
checkerFramework = { module = "org.checkerframework:checker-qual", version.ref = "checkerFramework" }
|
||||
cobalt = { module = "org.squiddev:Cobalt", version.ref = "cobalt" }
|
||||
commonsCli = { module = "commons-cli:commons-cli", version.ref = "commonsCli" }
|
||||
fastutil = { module = "it.unimi.dsi:fastutil", version.ref = "fastutil" }
|
||||
forgeSpi = { module = "net.minecraftforge:forgespi", version.ref = "forgeSpi" }
|
||||
guava = { module = "com.google.guava:guava", version.ref = "guava" }
|
||||
@@ -97,6 +99,7 @@ slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
|
||||
# Minecraft mods
|
||||
fabric-api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric-api" }
|
||||
fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" }
|
||||
fabric-junit = { module = "net.fabricmc:fabric-loader-junit", version.ref = "fabric-loader" }
|
||||
fabricPermissions = { module = "me.lucko:fabric-permissions-api", version.ref = "fabricPermissions" }
|
||||
emi = { module = "dev.emi:emi-xplat-mojmap", version.ref = "emi" }
|
||||
iris = { module = "maven.modrinth:iris", version.ref = "iris" }
|
||||
@@ -114,8 +117,6 @@ rubidium = { module = "maven.modrinth:rubidium", version.ref = "rubidium" }
|
||||
sodium = { module = "maven.modrinth:sodium", version.ref = "sodium" }
|
||||
|
||||
# Testing
|
||||
byteBuddyAgent = { module = "net.bytebuddy:byte-buddy-agent", version.ref = "byteBuddy" }
|
||||
byteBuddy = { module = "net.bytebuddy:byte-buddy", version.ref = "byteBuddy" }
|
||||
hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" }
|
||||
jqwik-api = { module = "net.jqwik:jqwik-api", version.ref = "jqwik" }
|
||||
jqwik-engine = { module = "net.jqwik:jqwik-engine", version.ref = "jqwik" }
|
||||
@@ -124,6 +125,12 @@ junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", vers
|
||||
junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" }
|
||||
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
|
||||
|
||||
# LWJGL
|
||||
lwjgl-bom = { module = "org.lwjgl:lwjgl-bom", version.ref = "lwjgl" }
|
||||
lwjgl-core = { module = "org.lwjgl:lwjgl" }
|
||||
lwjgl-opengl = { module = "org.lwjgl:lwjgl-opengl" }
|
||||
lwjgl-glfw = { module = "org.lwjgl:lwjgl-glfw" }
|
||||
|
||||
# Build tools
|
||||
cctJavadoc = { module = "cc.tweaked:cct-javadoc", version.ref = "cctJavadoc" }
|
||||
checkstyle = { module = "com.puppycrawl.tools:checkstyle", version.ref = "checkstyle" }
|
||||
@@ -135,6 +142,7 @@ errorProne-plugin = { module = "net.ltgt.gradle:gradle-errorprone-plugin", versi
|
||||
errorProne-testHelpers = { module = "com.google.errorprone:error_prone_test_helpers", version.ref = "errorProne-core" }
|
||||
fabric-loom = { module = "net.fabricmc:fabric-loom", version.ref = "fabric-loom" }
|
||||
forgeGradle = { module = "net.minecraftforge.gradle:ForgeGradle", version.ref = "forgeGradle" }
|
||||
ideaExt = { module = "gradle.plugin.org.jetbrains.gradle.plugin.idea-ext:gradle-idea-ext", version.ref = "ideaExt" }
|
||||
kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||
librarian = { module = "org.parchmentmc:librarian", version.ref = "librarian" }
|
||||
minotaur = { module = "com.modrinth.minotaur:Minotaur", version.ref = "minotaur" }
|
||||
@@ -154,7 +162,6 @@ vineflower = { module = "io.github.juuxel:loom-vineflower", version.ref = "vinef
|
||||
[plugins]
|
||||
forgeGradle = { id = "net.minecraftforge.gradle", version.ref = "forgeGradle" }
|
||||
githubRelease = { id = "com.github.breadmoirai.github-release", version.ref = "githubRelease" }
|
||||
ideaExt = { id = "org.jetbrains.gradle.plugin.idea-ext", version.ref = "ideaExt" }
|
||||
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
librarian = { id = "org.parchmentmc.librarian.forgegradle", version.ref = "librarian" }
|
||||
mixinGradle = { id = "org.spongepowered.mixin", version.ref = "mixinGradle" }
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
; SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
;
|
||||
; SPDX-License-Identifier: LicenseRef-CCPL
|
||||
; SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
(sources
|
||||
/doc/
|
||||
@@ -77,7 +77,6 @@
|
||||
(globals
|
||||
:max
|
||||
_CC_DEFAULT_SETTINGS
|
||||
_CC_DISABLE_LUA51_FEATURES
|
||||
_HOST
|
||||
;; Ideally we'd pick these up from bios.lua, but illuaminate currently
|
||||
;; isn't smart enough.
|
||||
|
@@ -62,6 +62,9 @@ mentioning:
|
||||
- `lints`: This defines an [ErrorProne] plugin which adds a couple of compile-time checks to our code. This is what
|
||||
enforces that no client-specific code is used inside the `main` source set (and a couple of other things!).
|
||||
|
||||
- `standalone`: This contains a standalone UI for computers, allowing debugging and development of CraftOS without
|
||||
launching Minecraft.
|
||||
|
||||
- `web`: This contains the additional tooling for building [the documentation website][tweaked.cc], such as support for
|
||||
rendering recipes
|
||||
|
||||
|
@@ -19,8 +19,6 @@ import net.minecraft.data.PackOutput;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.Item;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.*;
|
||||
@@ -36,8 +34,6 @@ import java.util.function.Function;
|
||||
* @param <R> The upgrade serialiser to register for.
|
||||
*/
|
||||
public abstract class UpgradeDataProvider<T extends UpgradeBase, R extends UpgradeSerialiser<? extends T>> implements DataProvider {
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
|
||||
private final PackOutput output;
|
||||
private final String name;
|
||||
private final String folder;
|
||||
|
@@ -7,9 +7,9 @@ import cc.tweaked.gradle.clientClasses
|
||||
import cc.tweaked.gradle.commonClasses
|
||||
|
||||
plugins {
|
||||
id("cc-tweaked.publishing")
|
||||
id("cc-tweaked.vanilla")
|
||||
id("cc-tweaked.gametest")
|
||||
id("cc-tweaked.publishing")
|
||||
}
|
||||
|
||||
minecraft {
|
||||
|
@@ -33,7 +33,6 @@ import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -219,7 +218,7 @@ public abstract class AbstractComputerScreen<T extends AbstractComputerMenu> ext
|
||||
|
||||
private void alert(Component title, Component message) {
|
||||
OptionScreen.show(minecraft, title, message,
|
||||
Collections.singletonList(OptionScreen.newButton(OK, b -> minecraft.setScreen(this))),
|
||||
List.of(OptionScreen.newButton(OK, b -> minecraft.setScreen(this))),
|
||||
() -> minecraft.setScreen(this)
|
||||
);
|
||||
}
|
||||
|
@@ -14,10 +14,10 @@ import dan200.computercraft.core.terminal.TextBuffer;
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
import net.minecraft.client.renderer.ShaderInstance;
|
||||
import net.minecraft.server.packs.resources.ResourceProvider;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.lwjgl.opengl.GL13;
|
||||
import org.lwjgl.opengl.GL31;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
@@ -36,12 +36,12 @@ import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.get
|
||||
* @see RenderTypes#getMonitorTextureBufferShader()
|
||||
*/
|
||||
public class MonitorTextureBufferShader extends ShaderInstance {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MonitorTextureBufferShader.class);
|
||||
|
||||
public static final int UNIFORM_SIZE = 4 * 4 * 16 + 4 + 4 + 2 * 4 + 4;
|
||||
|
||||
static final int TEXTURE_INDEX = GL13.GL_TEXTURE3;
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
|
||||
private final int monitorData;
|
||||
private int uniformBuffer = 0;
|
||||
|
||||
@@ -75,7 +75,7 @@ public class MonitorTextureBufferShader extends ShaderInstance {
|
||||
private Uniform getUniformChecked(String name) {
|
||||
var uniform = getUniform(name);
|
||||
if (uniform == null) {
|
||||
LOGGER.warn("Monitor shader {} should have uniform {}, but it was not present.", getName(), name);
|
||||
LOG.warn("Monitor shader {} should have uniform {}, but it was not present.", getName(), name);
|
||||
}
|
||||
|
||||
return uniform;
|
||||
|
@@ -213,7 +213,6 @@ public final class LanguageProvider implements DataProvider {
|
||||
addConfigEntry(ConfigSpec.floppySpaceLimit, "Floppy Disk space limit (bytes)");
|
||||
addConfigEntry(ConfigSpec.uploadMaxSize, "File upload size limit (bytes)");
|
||||
addConfigEntry(ConfigSpec.maximumFilesOpen, "Maximum files open per computer");
|
||||
addConfigEntry(ConfigSpec.disableLua51Features, "Disable Lua 5.1 features");
|
||||
addConfigEntry(ConfigSpec.defaultComputerSettings, "Default Computer settings");
|
||||
addConfigEntry(ConfigSpec.logComputerErrors, "Log computer errors");
|
||||
addConfigEntry(ConfigSpec.commandRequireCreative, "Command computers require creative");
|
||||
|
@@ -18,8 +18,8 @@ import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener;
|
||||
import net.minecraft.util.GsonHelper;
|
||||
import net.minecraft.util.profiling.ProfilerFiller;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collection;
|
||||
@@ -37,7 +37,7 @@ import java.util.stream.Collectors;
|
||||
* @see PocketUpgrades
|
||||
*/
|
||||
public class UpgradeManager<R extends UpgradeSerialiser<? extends T>, T extends UpgradeBase> extends SimpleJsonResourceReloadListener {
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UpgradeManager.class);
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
|
||||
|
||||
public record UpgradeWrapper<R extends UpgradeSerialiser<? extends T>, T extends UpgradeBase>(
|
||||
@@ -48,8 +48,8 @@ public class UpgradeManager<R extends UpgradeSerialiser<? extends T>, T extends
|
||||
private final String kind;
|
||||
private final ResourceKey<Registry<R>> registry;
|
||||
|
||||
private Map<String, UpgradeWrapper<R, T>> current = Collections.emptyMap();
|
||||
private Map<T, UpgradeWrapper<R, T>> currentWrappers = Collections.emptyMap();
|
||||
private Map<String, UpgradeWrapper<R, T>> current = Map.of();
|
||||
private Map<T, UpgradeWrapper<R, T>> currentWrappers = Map.of();
|
||||
|
||||
public UpgradeManager(String kind, String path, ResourceKey<Registry<R>> registry) {
|
||||
super(GSON, path);
|
||||
@@ -103,13 +103,13 @@ public class UpgradeManager<R extends UpgradeSerialiser<? extends T>, T extends
|
||||
try {
|
||||
loadUpgrade(newUpgrades, element.getKey(), element.getValue());
|
||||
} catch (IllegalArgumentException | JsonParseException e) {
|
||||
LOGGER.error("Error loading {} {} from JSON file", kind, element.getKey(), e);
|
||||
LOG.error("Error loading {} {} from JSON file", kind, element.getKey(), e);
|
||||
}
|
||||
}
|
||||
|
||||
current = Collections.unmodifiableMap(newUpgrades);
|
||||
currentWrappers = newUpgrades.values().stream().collect(Collectors.toUnmodifiableMap(UpgradeWrapper::upgrade, x -> x));
|
||||
LOGGER.info("Loaded {} {}s", current.size(), kind);
|
||||
LOG.info("Loaded {} {}s", current.size(), kind);
|
||||
}
|
||||
|
||||
private void loadUpgrade(Map<String, UpgradeWrapper<R, T>> current, ResourceLocation id, JsonElement json) {
|
||||
|
@@ -12,7 +12,7 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
final class WiredNetworkChangeImpl implements WiredNetworkChange {
|
||||
private static final WiredNetworkChangeImpl EMPTY = new WiredNetworkChangeImpl(Collections.emptyMap(), Collections.emptyMap());
|
||||
private static final WiredNetworkChangeImpl EMPTY = new WiredNetworkChangeImpl(Map.of(), Map.of());
|
||||
|
||||
private final Map<String, IPeripheral> removed;
|
||||
private final Map<String, IPeripheral> added;
|
||||
@@ -27,11 +27,11 @@ final class WiredNetworkChangeImpl implements WiredNetworkChange {
|
||||
}
|
||||
|
||||
static WiredNetworkChangeImpl added(Map<String, IPeripheral> added) {
|
||||
return added.isEmpty() ? EMPTY : new WiredNetworkChangeImpl(Collections.emptyMap(), Collections.unmodifiableMap(added));
|
||||
return added.isEmpty() ? EMPTY : new WiredNetworkChangeImpl(Map.of(), Collections.unmodifiableMap(added));
|
||||
}
|
||||
|
||||
static WiredNetworkChangeImpl removed(Map<String, IPeripheral> removed) {
|
||||
return removed.isEmpty() ? EMPTY : new WiredNetworkChangeImpl(Collections.unmodifiableMap(removed), Collections.emptyMap());
|
||||
return removed.isEmpty() ? EMPTY : new WiredNetworkChangeImpl(Collections.unmodifiableMap(removed), Map.of());
|
||||
}
|
||||
|
||||
static WiredNetworkChangeImpl changeOf(Map<String, IPeripheral> oldPeripherals, Map<String, IPeripheral> newPeripherals) {
|
||||
@@ -39,9 +39,9 @@ final class WiredNetworkChangeImpl implements WiredNetworkChange {
|
||||
if (oldPeripherals.isEmpty() && newPeripherals.isEmpty()) {
|
||||
return EMPTY;
|
||||
} else if (oldPeripherals.isEmpty()) {
|
||||
return new WiredNetworkChangeImpl(Collections.emptyMap(), newPeripherals);
|
||||
return new WiredNetworkChangeImpl(Map.of(), newPeripherals);
|
||||
} else if (newPeripherals.isEmpty()) {
|
||||
return new WiredNetworkChangeImpl(oldPeripherals, Collections.emptyMap());
|
||||
return new WiredNetworkChangeImpl(oldPeripherals, Map.of());
|
||||
}
|
||||
|
||||
Map<String, IPeripheral> added = new HashMap<>(newPeripherals);
|
||||
|
@@ -4,7 +4,6 @@
|
||||
|
||||
package dan200.computercraft.impl.network.wired;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import dan200.computercraft.api.network.Packet;
|
||||
import dan200.computercraft.api.network.wired.WiredNetwork;
|
||||
import dan200.computercraft.api.network.wired.WiredNode;
|
||||
@@ -57,10 +56,10 @@ final class WiredNetworkImpl implements WiredNetwork {
|
||||
// Move all nodes across into this network, destroying the original nodes.
|
||||
nodes.addAll(otherNodes);
|
||||
for (var node : otherNodes) node.network = this;
|
||||
other.nodes = Collections.emptySet();
|
||||
other.nodes = Set.of();
|
||||
|
||||
// Move all peripherals across,
|
||||
other.peripherals = Collections.emptyMap();
|
||||
other.peripherals = Map.of();
|
||||
peripherals.putAll(otherPeripherals);
|
||||
|
||||
if (!thisPeripherals.isEmpty()) {
|
||||
@@ -217,7 +216,7 @@ final class WiredNetworkImpl implements WiredNetwork {
|
||||
try {
|
||||
// We special case the original node: detaching all peripherals when needed.
|
||||
wired.network = wiredNetwork;
|
||||
wired.peripherals = Collections.emptyMap();
|
||||
wired.peripherals = Map.of();
|
||||
|
||||
// Ensure every network is finalised
|
||||
for (var network : maximals) {
|
||||
@@ -260,7 +259,7 @@ final class WiredNetworkImpl implements WiredNetwork {
|
||||
var change = WiredNetworkChangeImpl.changeOf(oldPeripherals, newPeripherals);
|
||||
if (change.isEmpty()) return;
|
||||
|
||||
wired.peripherals = ImmutableMap.copyOf(newPeripherals);
|
||||
wired.peripherals = Map.copyOf(newPeripherals);
|
||||
|
||||
// Detach the old peripherals then remove them.
|
||||
peripherals.keySet().removeAll(change.peripheralsRemoved().keySet());
|
||||
@@ -333,7 +332,7 @@ final class WiredNetworkImpl implements WiredNetwork {
|
||||
// Detach the old peripherals then remove them from the old network
|
||||
wired.network = wiredNetwork;
|
||||
wired.neighbours.clear();
|
||||
wired.peripherals = Collections.emptyMap();
|
||||
wired.peripherals = Map.of();
|
||||
|
||||
// Broadcast the change
|
||||
if (!peripherals.isEmpty()) WiredNetworkChangeImpl.removed(peripherals).broadcast(wired);
|
||||
|
@@ -13,13 +13,16 @@ import dan200.computercraft.api.network.wired.WiredSender;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.*;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public final class WiredNodeImpl implements WiredNode {
|
||||
private @Nullable Set<PacketReceiver> receivers;
|
||||
|
||||
final WiredElement element;
|
||||
Map<String, IPeripheral> peripherals = Collections.emptyMap();
|
||||
Map<String, IPeripheral> peripherals = Map.of();
|
||||
|
||||
final HashSet<WiredNodeImpl> neighbours = new HashSet<>();
|
||||
volatile WiredNetworkImpl network;
|
||||
|
@@ -89,7 +89,7 @@ public final class CommandComputerCraft {
|
||||
RequiredArgumentBuilder.<CommandSourceStack, ComputersArgumentType.ComputersSupplier>argument("computer", manyComputers())
|
||||
.suggests((context, builder) -> Suggestions.empty())
|
||||
)
|
||||
.argManyValue("args", StringArgumentType.string(), Collections.emptyList())
|
||||
.argManyValue("args", StringArgumentType.string(), List.of())
|
||||
.executes((c, a) -> queue(getComputersArgument(c, "computer"), a)))
|
||||
|
||||
.then(command("view")
|
||||
@@ -300,7 +300,7 @@ public final class CommandComputerCraft {
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static final List<AggregatedMetric> DEFAULT_FIELDS = Arrays.asList(
|
||||
private static final List<AggregatedMetric> DEFAULT_FIELDS = List.of(
|
||||
new AggregatedMetric(Metrics.COMPUTER_TASKS, Aggregate.COUNT),
|
||||
new AggregatedMetric(Metrics.COMPUTER_TASKS, Aggregate.NONE),
|
||||
new AggregatedMetric(Metrics.COMPUTER_TASKS, Aggregate.AVG)
|
||||
|
@@ -32,7 +32,7 @@ public final class ComputersArgumentType implements ArgumentType<ComputersArgume
|
||||
private static final ComputersArgumentType MANY = new ComputersArgumentType(false);
|
||||
private static final ComputersArgumentType SOME = new ComputersArgumentType(true);
|
||||
|
||||
private static final List<String> EXAMPLES = Arrays.asList(
|
||||
private static final List<String> EXAMPLES = List.of(
|
||||
"0", "#0", "@Label", "~Advanced"
|
||||
);
|
||||
|
||||
@@ -75,7 +75,7 @@ public final class ComputersArgumentType implements ArgumentType<ComputersArgume
|
||||
var instance = reader.readInt();
|
||||
computers = s -> {
|
||||
var computer = ServerContext.get(s.getServer()).registry().get(instance);
|
||||
return computer == null ? Collections.emptyList() : Collections.singletonList(computer);
|
||||
return computer == null ? List.of() : List.of(computer);
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -15,7 +15,6 @@ import net.minecraft.commands.CommandSourceStack;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
@@ -63,7 +62,7 @@ public class CommandBuilder<S> implements CommandNodeBuilder<S, Command<S>> {
|
||||
}
|
||||
|
||||
public <T> CommandNodeBuilder<S, ArgCommand<S, List<T>>> argManyValue(String name, ArgumentType<T> type, T defaultValue) {
|
||||
return argManyValue(name, type, Collections.singletonList(defaultValue));
|
||||
return argManyValue(name, type, List.of(defaultValue));
|
||||
}
|
||||
|
||||
public <T> CommandNodeBuilder<S, ArgCommand<S, List<T>>> argMany(String name, ArgumentType<T> type, Supplier<List<T>> empty) {
|
||||
|
@@ -134,12 +134,12 @@ public class CommandAPI implements ILuaAPI {
|
||||
public final List<String> list(IArguments args) throws LuaException {
|
||||
var server = computer.getLevel().getServer();
|
||||
|
||||
if (server == null) return Collections.emptyList();
|
||||
if (server == null) return List.of();
|
||||
CommandNode<CommandSourceStack> node = server.getCommands().getDispatcher().getRoot();
|
||||
for (var j = 0; j < args.count(); j++) {
|
||||
var name = args.getString(j);
|
||||
node = node.getChild(name);
|
||||
if (!(node instanceof LiteralCommandNode)) return Collections.emptyList();
|
||||
if (!(node instanceof LiteralCommandNode)) return List.of();
|
||||
}
|
||||
|
||||
List<String> result = new ArrayList<>();
|
||||
|
@@ -21,6 +21,8 @@ import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static dan200.computercraft.core.filesystem.MountHelpers.NO_SUCH_FILE;
|
||||
|
||||
/**
|
||||
* A mount backed by Minecraft's {@link ResourceManager}.
|
||||
*
|
||||
|
@@ -30,7 +30,6 @@ public final class ConfigSpec {
|
||||
public static final ConfigFile.Value<Integer> computerSpaceLimit;
|
||||
public static final ConfigFile.Value<Integer> floppySpaceLimit;
|
||||
public static final ConfigFile.Value<Integer> maximumFilesOpen;
|
||||
public static final ConfigFile.Value<Boolean> disableLua51Features;
|
||||
public static final ConfigFile.Value<String> defaultComputerSettings;
|
||||
public static final ConfigFile.Value<Boolean> logComputerErrors;
|
||||
public static final ConfigFile.Value<Boolean> commandRequireCreative;
|
||||
@@ -115,12 +114,6 @@ public final class ConfigSpec {
|
||||
.comment("Set how many files a computer can have open at the same time. Set to 0 for unlimited.")
|
||||
.defineInRange("maximum_open_files", CoreConfig.maximumFilesOpen, 0, Integer.MAX_VALUE);
|
||||
|
||||
disableLua51Features = builder
|
||||
.comment("""
|
||||
Set this to true to disable Lua 5.1 functions that will be removed in a future
|
||||
update. Useful for ensuring forward compatibility of your programs now.""")
|
||||
.define("disable_lua51_features", CoreConfig.disableLua51Features);
|
||||
|
||||
defaultComputerSettings = builder
|
||||
.comment("""
|
||||
A comma separated list of default system settings to set on new computers.
|
||||
@@ -395,7 +388,6 @@ public final class ConfigSpec {
|
||||
Config.floppySpaceLimit = floppySpaceLimit.get();
|
||||
Config.uploadMaxSize = uploadMaxSize.get();
|
||||
CoreConfig.maximumFilesOpen = maximumFilesOpen.get();
|
||||
CoreConfig.disableLua51Features = disableLua51Features.get();
|
||||
CoreConfig.defaultComputerSettings = defaultComputerSettings.get();
|
||||
Config.commandRequireCreative = commandRequireCreative.get();
|
||||
|
||||
|
@@ -12,7 +12,6 @@ import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
|
||||
import net.minecraft.world.level.storage.loot.predicates.LootItemCondition;
|
||||
import net.minecraft.world.level.storage.loot.predicates.LootItemConditionType;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -33,7 +32,7 @@ public final class BlockNamedEntityLootCondition implements LootItemCondition {
|
||||
|
||||
@Override
|
||||
public Set<LootContextParam<?>> getReferencedContextParams() {
|
||||
return Collections.singleton(LootContextParams.BLOCK_ENTITY);
|
||||
return Set.of(LootContextParams.BLOCK_ENTITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -12,7 +12,6 @@ import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
|
||||
import net.minecraft.world.level.storage.loot.predicates.LootItemCondition;
|
||||
import net.minecraft.world.level.storage.loot.predicates.LootItemConditionType;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -33,7 +32,7 @@ public final class HasComputerIdLootCondition implements LootItemCondition {
|
||||
|
||||
@Override
|
||||
public Set<LootContextParam<?>> getReferencedContextParams() {
|
||||
return Collections.singleton(LootContextParams.BLOCK_ENTITY);
|
||||
return Set.of(LootContextParams.BLOCK_ENTITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -12,7 +12,6 @@ import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
|
||||
import net.minecraft.world.level.storage.loot.predicates.LootItemCondition;
|
||||
import net.minecraft.world.level.storage.loot.predicates.LootItemConditionType;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -33,7 +32,7 @@ public final class PlayerCreativeLootCondition implements LootItemCondition {
|
||||
|
||||
@Override
|
||||
public Set<LootContextParam<?>> getReferencedContextParams() {
|
||||
return Collections.singleton(LootContextParams.THIS_ENTITY);
|
||||
return Set.of(LootContextParams.THIS_ENTITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -9,14 +9,12 @@ import dan200.computercraft.api.upgrades.UpgradeData;
|
||||
import dan200.computercraft.impl.PocketUpgrades;
|
||||
import dan200.computercraft.impl.TurtleUpgrades;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -24,7 +22,6 @@ import java.util.function.Supplier;
|
||||
* Utilities for recipe mod plugins (such as JEI).
|
||||
*/
|
||||
public final class RecipeModHelpers {
|
||||
static final List<ComputerFamily> MAIN_FAMILIES = Arrays.asList(ComputerFamily.NORMAL, ComputerFamily.ADVANCED);
|
||||
static final List<Supplier<TurtleItem>> TURTLES = List.of(ModRegistry.Items.TURTLE_NORMAL, ModRegistry.Items.TURTLE_ADVANCED);
|
||||
static final List<Supplier<PocketComputerItem>> POCKET_COMPUTERS = List.of(ModRegistry.Items.POCKET_COMPUTER_NORMAL, ModRegistry.Items.POCKET_COMPUTER_ADVANCED);
|
||||
|
||||
|
@@ -114,7 +114,7 @@ public class UpgradeRecipeGenerator<T> {
|
||||
// Suggest possible upgrades which can be applied to this turtle
|
||||
var left = item.getUpgradeWithData(stack, TurtleSide.LEFT);
|
||||
var right = item.getUpgradeWithData(stack, TurtleSide.RIGHT);
|
||||
if (left != null && right != null) return Collections.emptyList();
|
||||
if (left != null && right != null) return List.of();
|
||||
|
||||
List<T> recipes = new ArrayList<>();
|
||||
var ingredient = Ingredient.of(stack);
|
||||
@@ -135,7 +135,7 @@ public class UpgradeRecipeGenerator<T> {
|
||||
} else if (stack.getItem() instanceof PocketComputerItem) {
|
||||
// Suggest possible upgrades which can be applied to this turtle
|
||||
var back = PocketComputerItem.getUpgrade(stack);
|
||||
if (back != null) return Collections.emptyList();
|
||||
if (back != null) return List.of();
|
||||
|
||||
List<T> recipes = new ArrayList<>();
|
||||
var ingredient = Ingredient.of(stack);
|
||||
@@ -148,7 +148,7 @@ public class UpgradeRecipeGenerator<T> {
|
||||
} else {
|
||||
// If this item is usable as an upgrade, find all possible recipes.
|
||||
var upgrades = upgradeItemLookup.get(stack.getItem());
|
||||
if (upgrades == null) return Collections.emptyList();
|
||||
if (upgrades == null) return List.of();
|
||||
|
||||
List<T> recipes = null;
|
||||
var multiple = false;
|
||||
@@ -169,7 +169,7 @@ public class UpgradeRecipeGenerator<T> {
|
||||
}
|
||||
}
|
||||
|
||||
return recipes == null ? Collections.emptyList() : Collections.unmodifiableList(recipes);
|
||||
return recipes == null ? List.of() : Collections.unmodifiableList(recipes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ public class UpgradeRecipeGenerator<T> {
|
||||
|
||||
return Collections.unmodifiableList(recipes);
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -22,7 +22,7 @@ import mezz.jei.api.runtime.IJeiRuntime;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@JeiPlugin
|
||||
public class JEIComputerCraft implements IModPlugin {
|
||||
@@ -61,7 +61,7 @@ public class JEIComputerCraft implements IModPlugin {
|
||||
var category = registry.createRecipeLookup(RecipeTypes.CRAFTING);
|
||||
category.get().forEach(wrapper -> {
|
||||
if (RecipeModHelpers.shouldRemoveRecipe(wrapper.getId())) {
|
||||
registry.hideRecipes(RecipeTypes.CRAFTING, Collections.singleton(wrapper));
|
||||
registry.hideRecipes(RecipeTypes.CRAFTING, List.of(wrapper));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -15,7 +15,6 @@ import mezz.jei.api.recipe.category.IRecipeCategory;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.crafting.CraftingRecipe;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
class RecipeResolver implements IRecipeManagerPlugin {
|
||||
@@ -24,36 +23,36 @@ class RecipeResolver implements IRecipeManagerPlugin {
|
||||
@Override
|
||||
public <V> List<RecipeType<?>> getRecipeTypes(IFocus<V> focus) {
|
||||
var value = focus.getTypedValue().getIngredient();
|
||||
if (!(value instanceof ItemStack stack)) return Collections.emptyList();
|
||||
if (!(value instanceof ItemStack stack)) return List.of();
|
||||
|
||||
return switch (focus.getRole()) {
|
||||
case INPUT ->
|
||||
stack.getItem() instanceof TurtleItem || stack.getItem() instanceof PocketComputerItem || resolver.isUpgrade(stack)
|
||||
? Collections.singletonList(RecipeTypes.CRAFTING)
|
||||
: Collections.emptyList();
|
||||
? List.of(RecipeTypes.CRAFTING)
|
||||
: List.of();
|
||||
case OUTPUT -> stack.getItem() instanceof TurtleItem || stack.getItem() instanceof PocketComputerItem
|
||||
? Collections.singletonList(RecipeTypes.CRAFTING)
|
||||
: Collections.emptyList();
|
||||
default -> Collections.emptyList();
|
||||
? List.of(RecipeTypes.CRAFTING)
|
||||
: List.of();
|
||||
default -> List.of();
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T, V> List<T> getRecipes(IRecipeCategory<T> recipeCategory, IFocus<V> focus) {
|
||||
if (!(focus.getTypedValue().getIngredient() instanceof ItemStack stack) || recipeCategory.getRecipeType() != RecipeTypes.CRAFTING) {
|
||||
return Collections.emptyList();
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return switch (focus.getRole()) {
|
||||
case INPUT -> cast(resolver.findRecipesWithInput(stack));
|
||||
case OUTPUT -> cast(resolver.findRecipesWithOutput(stack));
|
||||
default -> Collections.emptyList();
|
||||
default -> List.of();
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> getRecipes(IRecipeCategory<T> recipeCategory) {
|
||||
return Collections.emptyList();
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
|
@@ -0,0 +1,36 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.peripheral.generic;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Extract some component (for instance a capability on Forge, or a {@code BlockApiLookup} on Fabric) from a block and
|
||||
* block entity.
|
||||
*
|
||||
* @param <C> A platform-specific type, used for the invalidation callback.
|
||||
*/
|
||||
public interface ComponentLookup<C extends Runnable> {
|
||||
/**
|
||||
* Extract some component from a block in the world.
|
||||
*
|
||||
* @param level The current level.
|
||||
* @param pos The position of the block in the level.
|
||||
* @param state The block state at that position.
|
||||
* @param blockEntity The block entity at that position.
|
||||
* @param side The side of the block to extract the component from. Implementations should try to use a
|
||||
* sideless lookup first, but may fall back to a sided lookup if needed.
|
||||
* @param invalidate An invalidation function to call if this component changes.
|
||||
* @return The found component, or {@code null} if not present.
|
||||
*/
|
||||
@Nullable
|
||||
Object find(Level level, BlockPos pos, BlockState state, BlockEntity blockEntity, Direction side, C invalidate);
|
||||
}
|
@@ -6,12 +6,9 @@ package dan200.computercraft.shared.peripheral.generic;
|
||||
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.api.peripheral.PeripheralType;
|
||||
import dan200.computercraft.core.methods.MethodSupplier;
|
||||
import dan200.computercraft.core.methods.NamedMethod;
|
||||
import dan200.computercraft.core.methods.PeripheralMethod;
|
||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
@@ -28,16 +25,10 @@ import java.util.Set;
|
||||
* See the platform-specific peripheral providers for the usage of this.
|
||||
*/
|
||||
final class GenericPeripheralBuilder {
|
||||
private final MethodSupplier<PeripheralMethod> peripheralMethods;
|
||||
|
||||
private @Nullable String name;
|
||||
private final Set<String> additionalTypes = new HashSet<>(0);
|
||||
private final ArrayList<SaturatedMethod> methods = new ArrayList<>();
|
||||
|
||||
GenericPeripheralBuilder(MinecraftServer server) {
|
||||
peripheralMethods = ServerContext.get(server).peripheralMethods();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
IPeripheral toPeripheral(BlockEntity blockEntity, Direction side) {
|
||||
if (methods.isEmpty()) return null;
|
||||
@@ -46,18 +37,16 @@ final class GenericPeripheralBuilder {
|
||||
return new GenericPeripheral(blockEntity, side, name, additionalTypes, methods);
|
||||
}
|
||||
|
||||
boolean addMethods(Object target) {
|
||||
return peripheralMethods.forEachSelfMethod(target, (name, method, info) -> {
|
||||
methods.add(new SaturatedMethod(target, name, method));
|
||||
void addMethod(Object target, String name, PeripheralMethod method, @Nullable NamedMethod<PeripheralMethod> info) {
|
||||
methods.add(new SaturatedMethod(target, name, method));
|
||||
|
||||
// If we have a peripheral type, use it. Always pick the smallest one, so it's consistent (assuming mods
|
||||
// don't change).
|
||||
var type = info == null ? null : info.genericType();
|
||||
if (type != null && type.getPrimaryType() != null) {
|
||||
var primaryType = type.getPrimaryType();
|
||||
if (this.name == null || this.name.compareTo(primaryType) > 0) this.name = primaryType;
|
||||
}
|
||||
if (type != null) additionalTypes.addAll(type.getAdditionalTypes());
|
||||
});
|
||||
// If we have a peripheral type, use it. Always pick the smallest one, so it's consistent (assuming mods
|
||||
// don't change).
|
||||
var type = info == null ? null : info.genericType();
|
||||
if (type != null && type.getPrimaryType() != null) {
|
||||
var primaryType = type.getPrimaryType();
|
||||
if (this.name == null || this.name.compareTo(primaryType) > 0) this.name = primaryType;
|
||||
}
|
||||
if (type != null) additionalTypes.addAll(type.getAdditionalTypes());
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,69 @@
|
||||
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.peripheral.generic;
|
||||
|
||||
import dan200.computercraft.api.lua.GenericSource;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.core.methods.MethodSupplier;
|
||||
import dan200.computercraft.core.methods.PeripheralMethod;
|
||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A peripheral provider which finds methods from various {@linkplain GenericSource generic sources}.
|
||||
* <p>
|
||||
* Methods are found using the original block entity itself and a registered list of {@link ComponentLookup}s.
|
||||
*
|
||||
* @param <C> A platform-specific type, used for the invalidation callback.
|
||||
*/
|
||||
public final class GenericPeripheralProvider<C extends Runnable> {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GenericPeripheralProvider.class);
|
||||
|
||||
private final List<ComponentLookup<? super C>> lookups = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Register a component lookup function.
|
||||
*
|
||||
* @param lookup The component lookup function.
|
||||
*/
|
||||
public synchronized void registerLookup(ComponentLookup<? super C> lookup) {
|
||||
Objects.requireNonNull(lookup);
|
||||
if (!lookups.contains(lookup)) lookups.add(lookup);
|
||||
}
|
||||
|
||||
public void forEachMethod(MethodSupplier<PeripheralMethod> methods, Level level, BlockPos pos, Direction side, BlockEntity blockEntity, C invalidate, MethodSupplier.TargetedConsumer<PeripheralMethod> consumer) {
|
||||
methods.forEachMethod(blockEntity, consumer);
|
||||
|
||||
for (var lookup : lookups) {
|
||||
var contents = lookup.find(level, pos, blockEntity.getBlockState(), blockEntity, side, invalidate);
|
||||
if (contents != null) methods.forEachMethod(contents, consumer);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public IPeripheral getPeripheral(Level level, BlockPos pos, Direction side, @Nullable BlockEntity blockEntity, C invalidate) {
|
||||
if (blockEntity == null) return null;
|
||||
|
||||
var server = level.getServer();
|
||||
if (server == null) {
|
||||
LOG.warn("Fetching peripherals on a non-server level {}.", level, new IllegalStateException("Fetching peripherals on a non-server level."));
|
||||
return null;
|
||||
}
|
||||
|
||||
var builder = new GenericPeripheralBuilder();
|
||||
forEachMethod(ServerContext.get(server).peripheralMethods(), level, pos, side, blockEntity, invalidate, builder::addMethod);
|
||||
return builder.toPeripheral(blockEntity, side);
|
||||
}
|
||||
}
|
@@ -4,12 +4,12 @@
|
||||
|
||||
package dan200.computercraft.shared.peripheral.modem.wired;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import dan200.computercraft.annotations.ForgeOverride;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.platform.PlatformHelper;
|
||||
import dan200.computercraft.shared.util.WaterloggableHelpers;
|
||||
import dan200.computercraft.shared.util.WorldUtil;
|
||||
import net.minecraft.Util;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
@@ -52,12 +52,14 @@ public class CableBlock extends Block implements SimpleWaterloggedBlock, EntityB
|
||||
public static final BooleanProperty UP = BooleanProperty.create("up");
|
||||
public static final BooleanProperty DOWN = BooleanProperty.create("down");
|
||||
|
||||
static final EnumMap<Direction, BooleanProperty> CONNECTIONS =
|
||||
new EnumMap<>(new ImmutableMap.Builder<Direction, BooleanProperty>()
|
||||
.put(Direction.DOWN, DOWN).put(Direction.UP, UP)
|
||||
.put(Direction.NORTH, NORTH).put(Direction.SOUTH, SOUTH)
|
||||
.put(Direction.WEST, WEST).put(Direction.EAST, EAST)
|
||||
.build());
|
||||
static final EnumMap<Direction, BooleanProperty> CONNECTIONS = Util.make(new EnumMap<>(Direction.class), m -> {
|
||||
m.put(Direction.DOWN, DOWN);
|
||||
m.put(Direction.UP, UP);
|
||||
m.put(Direction.NORTH, NORTH);
|
||||
m.put(Direction.SOUTH, SOUTH);
|
||||
m.put(Direction.WEST, WEST);
|
||||
m.put(Direction.EAST, EAST);
|
||||
});
|
||||
|
||||
public CableBlock(Properties settings) {
|
||||
super(settings);
|
||||
|
@@ -4,7 +4,6 @@
|
||||
|
||||
package dan200.computercraft.shared.peripheral.modem.wired;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import dan200.computercraft.api.network.wired.WiredElement;
|
||||
import dan200.computercraft.api.network.wired.WiredNode;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
@@ -31,7 +30,8 @@ import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public class CableBlockEntity extends BlockEntity {
|
||||
private static final String NBT_PERIPHERAL_ENABLED = "PeripheralAccess";
|
||||
@@ -181,7 +181,7 @@ public class CableBlockEntity extends BlockEntity {
|
||||
var oldName = peripheral.getConnectedName();
|
||||
togglePeripheralAccess();
|
||||
var newName = peripheral.getConnectedName();
|
||||
if (!Objects.equal(newName, oldName)) {
|
||||
if (!Objects.equals(newName, oldName)) {
|
||||
if (oldName != null) {
|
||||
player.displayClientMessage(Component.translatable("chat.computercraft.wired_modem.peripheral_disconnected",
|
||||
ChatHelpers.copy(oldName)), false);
|
||||
@@ -274,7 +274,7 @@ public class CableBlockEntity extends BlockEntity {
|
||||
if (!canAttachPeripheral() && peripheralAccessAllowed) {
|
||||
peripheralAccessAllowed = false;
|
||||
peripheral.detach();
|
||||
node.updatePeripherals(Collections.emptyMap());
|
||||
node.updatePeripherals(Map.of());
|
||||
setChanged();
|
||||
updateBlockState();
|
||||
}
|
||||
@@ -291,7 +291,7 @@ public class CableBlockEntity extends BlockEntity {
|
||||
peripheral.detach();
|
||||
|
||||
peripheralAccessAllowed = false;
|
||||
node.updatePeripherals(Collections.emptyMap());
|
||||
node.updatePeripherals(Map.of());
|
||||
}
|
||||
|
||||
updateBlockState();
|
||||
|
@@ -4,9 +4,9 @@
|
||||
|
||||
package dan200.computercraft.shared.peripheral.modem.wired;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import dan200.computercraft.shared.peripheral.modem.ModemShapes;
|
||||
import dan200.computercraft.shared.util.DirectionUtil;
|
||||
import net.minecraft.Util;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.shapes.Shapes;
|
||||
@@ -21,16 +21,14 @@ public final class CableShapes {
|
||||
private static final double MAX = 1 - MIN;
|
||||
|
||||
private static final VoxelShape SHAPE_CABLE_CORE = Shapes.box(MIN, MIN, MIN, MAX, MAX, MAX);
|
||||
private static final EnumMap<Direction, VoxelShape> SHAPE_CABLE_ARM =
|
||||
new EnumMap<>(new ImmutableMap.Builder<Direction, VoxelShape>()
|
||||
.put(Direction.DOWN, Shapes.box(MIN, 0, MIN, MAX, MIN, MAX))
|
||||
.put(Direction.UP, Shapes.box(MIN, MAX, MIN, MAX, 1, MAX))
|
||||
.put(Direction.NORTH, Shapes.box(MIN, MIN, 0, MAX, MAX, MIN))
|
||||
.put(Direction.SOUTH, Shapes.box(MIN, MIN, MAX, MAX, MAX, 1))
|
||||
.put(Direction.WEST, Shapes.box(0, MIN, MIN, MIN, MAX, MAX))
|
||||
.put(Direction.EAST, Shapes.box(MAX, MIN, MIN, 1, MAX, MAX))
|
||||
.build()
|
||||
);
|
||||
private static final EnumMap<Direction, VoxelShape> SHAPE_CABLE_ARM = Util.make(new EnumMap<>(Direction.class), m -> {
|
||||
m.put(Direction.DOWN, Shapes.box(MIN, 0, MIN, MAX, MIN, MAX));
|
||||
m.put(Direction.UP, Shapes.box(MIN, MAX, MIN, MAX, 1, MAX));
|
||||
m.put(Direction.NORTH, Shapes.box(MIN, MIN, 0, MAX, MAX, MIN));
|
||||
m.put(Direction.SOUTH, Shapes.box(MIN, MIN, MAX, MAX, MAX, 1));
|
||||
m.put(Direction.WEST, Shapes.box(0, MIN, MIN, MIN, MAX, MAX));
|
||||
m.put(Direction.EAST, Shapes.box(MAX, MIN, MIN, 1, MAX, MAX));
|
||||
});
|
||||
|
||||
private static final VoxelShape[] SHAPES = new VoxelShape[(1 << 6) * 7];
|
||||
private static final VoxelShape[] CABLE_SHAPES = new VoxelShape[1 << 6];
|
||||
|
@@ -4,7 +4,6 @@
|
||||
|
||||
package dan200.computercraft.shared.peripheral.modem.wired;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import dan200.computercraft.api.network.wired.WiredElement;
|
||||
import dan200.computercraft.api.network.wired.WiredNode;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
@@ -133,7 +132,7 @@ public class WiredModemFullBlockEntity extends BlockEntity {
|
||||
togglePeripheralAccess();
|
||||
var periphNames = getConnectedPeripheralNames();
|
||||
|
||||
if (!Objects.equal(periphNames, oldPeriphNames)) {
|
||||
if (!Objects.equals(periphNames, oldPeriphNames)) {
|
||||
sendPeripheralChanges(player, "chat.computercraft.wired_modem.peripheral_disconnected", oldPeriphNames);
|
||||
sendPeripheralChanges(player, "chat.computercraft.wired_modem.peripheral_connected", periphNames);
|
||||
}
|
||||
@@ -241,14 +240,14 @@ public class WiredModemFullBlockEntity extends BlockEntity {
|
||||
peripheralAccessAllowed = false;
|
||||
|
||||
for (var peripheral : peripherals) peripheral.detach();
|
||||
node.updatePeripherals(Collections.emptyMap());
|
||||
node.updatePeripherals(Map.of());
|
||||
}
|
||||
|
||||
updateBlockState();
|
||||
}
|
||||
|
||||
private Set<String> getConnectedPeripheralNames() {
|
||||
if (!peripheralAccessAllowed) return Collections.emptySet();
|
||||
if (!peripheralAccessAllowed) return Set.of();
|
||||
|
||||
Set<String> peripherals = new HashSet<>(6);
|
||||
for (var peripheral : this.peripherals) {
|
||||
@@ -259,7 +258,7 @@ public class WiredModemFullBlockEntity extends BlockEntity {
|
||||
}
|
||||
|
||||
private Map<String, IPeripheral> getConnectedPeripherals() {
|
||||
if (!peripheralAccessAllowed) return Collections.emptyMap();
|
||||
if (!peripheralAccessAllowed) return Map.of();
|
||||
|
||||
Map<String, IPeripheral> peripherals = new HashMap<>(6);
|
||||
for (var peripheral : this.peripherals) peripheral.extendMap(peripherals);
|
||||
|
@@ -103,7 +103,7 @@ public final class WiredModemLocalPeripheral {
|
||||
|
||||
public Map<String, IPeripheral> toMap() {
|
||||
return peripheral == null
|
||||
? Collections.emptyMap()
|
||||
? Map.of()
|
||||
: Collections.singletonMap(type + "_" + id, peripheral);
|
||||
}
|
||||
|
||||
|
@@ -4,7 +4,6 @@
|
||||
|
||||
package dan200.computercraft.shared.peripheral.modem.wired;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import dan200.computercraft.api.filesystem.Mount;
|
||||
import dan200.computercraft.api.filesystem.WritableMount;
|
||||
import dan200.computercraft.api.lua.*;
|
||||
@@ -69,7 +68,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
|
||||
@Override
|
||||
public Set<String> getAdditionalTypes() {
|
||||
return Collections.singleton("peripheral_hub");
|
||||
return Set.of("peripheral_hub");
|
||||
}
|
||||
|
||||
//region Peripheral methods
|
||||
@@ -89,7 +88,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
@LuaFunction
|
||||
public final Collection<String> getNamesRemote(IComputerAccess computer) {
|
||||
var wrappers = getWrappers(computer);
|
||||
return wrappers == null ? Collections.emptySet() : wrappers.keySet();
|
||||
return wrappers == null ? Set.of() : wrappers.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -429,7 +428,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
public Map<String, IPeripheral> getAvailablePeripherals() {
|
||||
if (!attached) throw new NotAttachedException();
|
||||
synchronized (element.getRemotePeripherals()) {
|
||||
return ImmutableMap.copyOf(element.getRemotePeripherals());
|
||||
return Map.copyOf(element.getRemotePeripherals());
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -157,7 +157,6 @@ public class MonitorBlockEntity extends BlockEntity {
|
||||
if (xIndex == 0 && yIndex == 0) {
|
||||
// If we're the origin, set up the new monitor
|
||||
serverMonitor = new ServerMonitor(advanced, this);
|
||||
serverMonitor.rebuild();
|
||||
|
||||
// And propagate it to child monitors
|
||||
for (var x = 0; x < width; x++) {
|
||||
@@ -178,6 +177,11 @@ public class MonitorBlockEntity extends BlockEntity {
|
||||
}
|
||||
}
|
||||
|
||||
private void createServerTerminal() {
|
||||
var monitor = createServerMonitor();
|
||||
if (monitor != null && monitor.getTerminal() == null) monitor.rebuild();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ClientMonitor getClientMonitor() {
|
||||
if (clientMonitor != null) return clientMonitor;
|
||||
@@ -377,6 +381,8 @@ public class MonitorBlockEntity extends BlockEntity {
|
||||
BlockEntityHelpers.updateBlock(monitor);
|
||||
}
|
||||
}
|
||||
|
||||
assertInvariant();
|
||||
}
|
||||
|
||||
void updateNeighborsDeferred() {
|
||||
@@ -487,9 +493,10 @@ public class MonitorBlockEntity extends BlockEntity {
|
||||
}
|
||||
|
||||
public IPeripheral peripheral() {
|
||||
createServerMonitor();
|
||||
if (peripheral != null) return peripheral;
|
||||
return peripheral = new MonitorPeripheral(this);
|
||||
createServerTerminal();
|
||||
var peripheral = this.peripheral != null ? this.peripheral : (this.peripheral = new MonitorPeripheral(this));
|
||||
assertInvariant();
|
||||
return peripheral;
|
||||
}
|
||||
|
||||
void addComputer(IComputerAccess computer) {
|
||||
@@ -528,4 +535,85 @@ public class MonitorBlockEntity extends BlockEntity {
|
||||
Math.max(startPos.getZ(), endPos.getZ()) + 1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert all {@linkplain #checkInvariants() monitor invariants} hold.
|
||||
*/
|
||||
private void assertInvariant() {
|
||||
assert checkInvariants() : "Monitor invariants failed. See logs.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check various invariants about this monitor multiblock. This is only called when assertions are enabled, so
|
||||
* will be skipped outside of tests.
|
||||
*
|
||||
* @return Whether all invariants passed.
|
||||
*/
|
||||
private boolean checkInvariants() {
|
||||
LOG.debug("Checking monitor invariants at {}", getBlockPos());
|
||||
|
||||
var okay = true;
|
||||
|
||||
if (width <= 0 || height <= 0) {
|
||||
okay = false;
|
||||
LOG.error("Monitor {} has non-positive of {}x{}", getBlockPos(), width, height);
|
||||
}
|
||||
|
||||
var hasPeripheral = false;
|
||||
var origin = getOrigin().getMonitor();
|
||||
var serverMonitor = origin != null ? origin.serverMonitor : this.serverMonitor;
|
||||
for (var x = 0; x < width; x++) {
|
||||
for (var y = 0; y < height; y++) {
|
||||
var monitor = getLoadedMonitor(x, y).getMonitor();
|
||||
if (monitor == null) continue;
|
||||
|
||||
hasPeripheral |= monitor.peripheral != null;
|
||||
|
||||
if (monitor.serverMonitor != null && monitor.serverMonitor != serverMonitor) {
|
||||
okay = false;
|
||||
LOG.error(
|
||||
"Monitor {} expected to be have serverMonitor={}, but was {}",
|
||||
monitor.getBlockPos(), serverMonitor, monitor.serverMonitor
|
||||
);
|
||||
}
|
||||
|
||||
if (monitor.xIndex != x || monitor.yIndex != y) {
|
||||
okay = false;
|
||||
LOG.error(
|
||||
"Monitor {} expected to be at {},{}, but believes it is {},{}",
|
||||
monitor.getBlockPos(), x, y, monitor.xIndex, monitor.yIndex
|
||||
);
|
||||
}
|
||||
|
||||
if (monitor.width != width || monitor.height != height) {
|
||||
okay = false;
|
||||
LOG.error(
|
||||
"Monitor {} expected to be size {},{}, but believes it is {},{}",
|
||||
monitor.getBlockPos(), width, height, monitor.width, monitor.height
|
||||
);
|
||||
}
|
||||
|
||||
var expectedState = getBlockState().setValue(MonitorBlock.STATE, MonitorEdgeState.fromConnections(
|
||||
y < height - 1, y > 0, x > 0, x < width - 1
|
||||
));
|
||||
if (monitor.getBlockState() != expectedState) {
|
||||
okay = false;
|
||||
LOG.error(
|
||||
"Monitor {} expected to have state {}, but has state {}",
|
||||
monitor.getBlockState(), expectedState, monitor.getBlockState()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasPeripheral != (serverMonitor != null && serverMonitor.getTerminal() != null)) {
|
||||
okay = false;
|
||||
LOG.error(
|
||||
"Peripheral is {}, but serverMonitor={} and serverMonitor.terminal={}",
|
||||
hasPeripheral, serverMonitor, serverMonitor == null ? null : serverMonitor.getTerminal()
|
||||
);
|
||||
}
|
||||
|
||||
return okay;
|
||||
}
|
||||
}
|
||||
|
@@ -107,7 +107,7 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
|
||||
@Override
|
||||
@Deprecated(forRemoval = true)
|
||||
public Map<ResourceLocation, IPeripheral> getUpgrades() {
|
||||
return upgrade == null ? Collections.emptyMap() : Collections.singletonMap(upgrade.getUpgradeID(), getPeripheral(ComputerSide.BACK));
|
||||
return upgrade == null ? Map.of() : Collections.singletonMap(upgrade.getUpgradeID(), getPeripheral(ComputerSide.BACK));
|
||||
}
|
||||
|
||||
public @Nullable UpgradeData<IPocketUpgrade> getUpgrade() {
|
||||
|
@@ -4,7 +4,6 @@
|
||||
|
||||
package dan200.computercraft.shared.pocket.items;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import dan200.computercraft.annotations.ForgeOverride;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.filesystem.Mount;
|
||||
@@ -44,6 +43,7 @@ import net.minecraft.world.level.Level;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class PocketComputerItem extends Item implements IComputerItem, IMedia, IColouredItem {
|
||||
private static final String NBT_UPGRADE = "Upgrade";
|
||||
@@ -97,7 +97,7 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
|
||||
|
||||
// Sync label
|
||||
var label = computer.getLabel();
|
||||
if (!Objects.equal(label, getLabel(stack))) {
|
||||
if (!Objects.equals(label, getLabel(stack))) {
|
||||
changed = true;
|
||||
setLabel(stack, label);
|
||||
}
|
||||
|
@@ -4,7 +4,6 @@
|
||||
|
||||
package dan200.computercraft.shared.turtle.core;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import dan200.computercraft.api.lua.ILuaCallback;
|
||||
import dan200.computercraft.api.lua.MethodResult;
|
||||
@@ -455,7 +454,7 @@ public class TurtleBrain implements TurtleAccessInternal {
|
||||
}
|
||||
|
||||
public void setOverlay(@Nullable ResourceLocation overlay) {
|
||||
if (!Objects.equal(this.overlay, overlay)) {
|
||||
if (!Objects.equals(this.overlay, overlay)) {
|
||||
this.overlay = overlay;
|
||||
BlockEntityHelpers.updateBlock(owner);
|
||||
}
|
||||
@@ -573,7 +572,7 @@ public class TurtleBrain implements TurtleAccessInternal {
|
||||
|
||||
public float getToolRenderAngle(TurtleSide side, float f) {
|
||||
return (side == TurtleSide.LEFT && animation == TurtleAnimation.SWING_LEFT_TOOL) ||
|
||||
(side == TurtleSide.RIGHT && animation == TurtleAnimation.SWING_RIGHT_TOOL)
|
||||
(side == TurtleSide.RIGHT && animation == TurtleAnimation.SWING_RIGHT_TOOL)
|
||||
? 45.0f * (float) Math.sin(getAnimationFraction(f) * Math.PI)
|
||||
: 0.0f;
|
||||
}
|
||||
|
@@ -27,7 +27,6 @@ import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.item.context.UseOnContext;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.SignBlockEntity;
|
||||
import net.minecraft.world.level.block.entity.SignText;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
@@ -186,7 +185,7 @@ public class TurtlePlaceCommand implements TurtleCommand {
|
||||
tile = world.getBlockEntity(position.relative(side));
|
||||
}
|
||||
|
||||
if (tile instanceof SignBlockEntity) setSignText(world, tile, message);
|
||||
if (tile instanceof SignBlockEntity sign) setSignText(world, sign, message);
|
||||
}
|
||||
|
||||
return placed;
|
||||
@@ -220,23 +219,20 @@ public class TurtlePlaceCommand implements TurtleCommand {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
private static void setSignText(Level world, BlockEntity tile, String message) {
|
||||
var signTile = (SignBlockEntity) tile;
|
||||
var split = Splitter.on('\n').splitToList(message);
|
||||
var firstLine = split.size() <= 2 ? 1 : 0;
|
||||
private static void setSignText(Level world, SignBlockEntity sign, String message) {
|
||||
var lines = Splitter.on('\n').splitToList(message);
|
||||
var firstLine = lines.size() <= 2 ? 1 : 0;
|
||||
|
||||
var signText = new SignText();
|
||||
for (var i = 0; i < 4; i++) {
|
||||
if (i >= firstLine && i < firstLine + split.size()) {
|
||||
var line = split.get(i - firstLine);
|
||||
signText.setMessage(i, line.length() > 15
|
||||
? Component.literal(line.substring(0, 15))
|
||||
: Component.literal(line)
|
||||
);
|
||||
}
|
||||
for (int i = 0, len = Math.min(lines.size(), 4); i < len; i++) {
|
||||
var line = lines.get(i);
|
||||
signText = signText.setMessage(i + firstLine, line.length() > 15
|
||||
? Component.literal(line.substring(0, 15))
|
||||
: Component.literal(line)
|
||||
);
|
||||
}
|
||||
signTile.setText(signText, true);
|
||||
world.sendBlockUpdated(tile.getBlockPos(), tile.getBlockState(), tile.getBlockState(), Block.UPDATE_ALL);
|
||||
sign.setText(signText, true);
|
||||
world.sendBlockUpdated(sign.getBlockPos(), sign.getBlockState(), sign.getBlockState(), Block.UPDATE_ALL);
|
||||
}
|
||||
|
||||
private static final class ErrorMessage {
|
||||
|
@@ -70,7 +70,7 @@ public class TurtleInventoryCrafting implements CraftingContainer {
|
||||
if (recipe == null) return null;
|
||||
|
||||
// Special case: craft(0) just returns an empty list if crafting was possible
|
||||
if (maxCount == 0) return Collections.emptyList();
|
||||
if (maxCount == 0) return List.of();
|
||||
|
||||
var player = TurtlePlayer.get(turtle).player();
|
||||
|
||||
|
@@ -20,7 +20,6 @@ import java.io.OutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -38,7 +37,7 @@ public final class NBTUtil {
|
||||
try {
|
||||
var ctor = CompoundTag.class.getDeclaredConstructor(Map.class);
|
||||
ctor.setAccessible(true);
|
||||
EMPTY_TAG = ctor.newInstance(Collections.emptyMap());
|
||||
EMPTY_TAG = ctor.newInstance(Map.of());
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
@@ -74,8 +74,6 @@
|
||||
"gui.computercraft.config.computer_space_limit.tooltip": "Limit diskového místa pro počítače a roboty, v bytech.",
|
||||
"gui.computercraft.config.default_computer_settings": "Vychozí nastavení počítače",
|
||||
"gui.computercraft.config.default_computer_settings.tooltip": "Čárkami oddělený seznam základních systémových nastavení pro nastavení na nových počítačích.\nPříklad: \"shell.autocomplete=false,lua.autocomplete=false,edit.autocomplete=false\"\nvypne všechno automatické vyplňování.",
|
||||
"gui.computercraft.config.disable_lua51_features": "Vypnout Lua 5.1 funkce",
|
||||
"gui.computercraft.config.disable_lua51_features.tooltip": "Nastav toto na pravdivou hodnotu pro vypnutí Lua 5.1 funkcí které budou odstraněny v budoucí\naktualizaci. Dobré pro ověření kompatibilty tvých programů nyní a předem.",
|
||||
"gui.computercraft.config.disabled_generic_methods": "Vypnuté obecné metody",
|
||||
"gui.computercraft.config.execution": "Spuštení",
|
||||
"gui.computercraft.config.execution.computer_threads": "Počítačová vlákna",
|
||||
|
@@ -27,6 +27,7 @@
|
||||
"commands.computercraft.desc": "Der /computercraft Befehl enthält verschiedene Werkzeuge um Computer zu debuggen, kontrollieren oder mit ihnen zu interagieren.",
|
||||
"commands.computercraft.dump.action": "Zeigt mehr Informationen über einen Computer",
|
||||
"commands.computercraft.dump.desc": "Zeigt den Status aller Computer oder genauere Informationen über einen angegebenen Computer. Der Computer kann entweder über seine Instanz ID (z.B. 123), seine Computer ID (z.B. #123) oder seinen Namen (z.B. \"@Mein Computer\") angegeben werden.",
|
||||
"commands.computercraft.dump.open_path": "Siehe die Dateien dieses Computers.",
|
||||
"commands.computercraft.dump.synopsis": "Zeigt den Status eines Computers.",
|
||||
"commands.computercraft.generic.additional_rows": "%d zusätzliche Zeilen…",
|
||||
"commands.computercraft.generic.exception": "Unbehandelte Ausnahme (%s)",
|
||||
@@ -68,12 +69,11 @@
|
||||
"commands.computercraft.view.not_player": "Konnte Terminal für Nicht-Spieler nicht öffnen",
|
||||
"commands.computercraft.view.synopsis": "Zeigt das Terminal eines Computers.",
|
||||
"gui.computercraft.config.command_require_creative": "Kommando-Computer benötigen den Kreativ-Modus",
|
||||
"gui.computercraft.config.command_require_creative.tooltip": "Benötigt Spieler sowohl im Kreativ-Spielmodus, als auch ein Operator zu sein,\num mit Kommando-Computern zu interagieren. Dies entspricht dem standardmäßigen Verhalten von Befehlsblöcken.",
|
||||
"gui.computercraft.config.computer_space_limit": "Speicherplatz von Computern (Bytes)",
|
||||
"gui.computercraft.config.computer_space_limit.tooltip": "Das Speicherplatzlimit für Computer und Turtles in Bytes.",
|
||||
"gui.computercraft.config.default_computer_settings": "Computer-Standardeinstellungen",
|
||||
"gui.computercraft.config.default_computer_settings.tooltip": "eine mit Komma separierte Liste an standardmäßige Systemeinstellungen für neuen Computern.\nBeispiel: \"shell.autocomplete=false,lua.autocomplete=false,edit.autocomplete=false\"\nwürde jegliche Autovervollständigung deaktivieren.",
|
||||
"gui.computercraft.config.disable_lua51_features": "Lua 5.1-Funktionen deaktivieren",
|
||||
"gui.computercraft.config.disable_lua51_features.tooltip": "Beim aktivieren werden die Lua 5.1 Funktionieren deaktiviert, die in zukünftigen\nUpdates sowieso entfernt werden. Auf dieser Weise kann vorwärts Kompatibilität für Programme sichergestellt werden.",
|
||||
"gui.computercraft.config.disabled_generic_methods": "Generische Methoden deaktiviert.",
|
||||
"gui.computercraft.config.disabled_generic_methods.tooltip": "Eine Liste an generischen Methoden oder Methodenquellen zum deaktivieren.\nGenerische Methoden sind Methoden die zu einem block/block entity hinzugefügt werden, insofern kein expliziter Peripheral Provider\ngefunden wurde. Mitbetroffen sind Inventarmethoden (d.h. inventory.getItemDetail,\ninventory.pushItems) und, wenn in Forge gespielt wird, die fluid_storage und energy_storage\nMethoden.\nMethoden in dieser Liste können entweder Gruppen von Methoden (wie computercraft:inventory)\noder einzelne Methoden (wie computercraft:inventory#pushItems) sein.\n",
|
||||
"gui.computercraft.config.execution": "Ausführung",
|
||||
|
@@ -33,7 +33,6 @@
|
||||
"commands.computercraft.shutdown.synopsis": "Apague las computadoras de forma remota.",
|
||||
"gui.computercraft.config.computer_space_limit": "Límite de memoria de ordenadores (en bytes)",
|
||||
"gui.computercraft.config.default_computer_settings": "Configuración de Ordenador por defecto",
|
||||
"gui.computercraft.config.disable_lua51_features": "Deshabilitar funciones de Lua 5.1",
|
||||
"gui.computercraft.config.floppy_space_limit": "Límite de memoria de disquetes (en bytes)",
|
||||
"gui.computercraft.config.http.enabled": "Habilitar API de HTTP",
|
||||
"gui.computercraft.config.log_computer_errors": "Grabar errores periféricos",
|
||||
|
@@ -74,9 +74,8 @@
|
||||
"gui.computercraft.config.computer_space_limit.tooltip": "La limite d'espace du disque pour les ordinateurs et les tortues, en octets.",
|
||||
"gui.computercraft.config.default_computer_settings": "Configuration d'Ordinateur par défaut",
|
||||
"gui.computercraft.config.default_computer_settings.tooltip": "Une liste séparée par des virgules des paramètres système par défaut à définir sur les nouveaux ordinateurs.\nExemple : \"shell.autocomplete=false,lua.autocomplete=false,edit.autocomplete=false\"\ndésactivera toute saisie semi-automatique.",
|
||||
"gui.computercraft.config.disable_lua51_features": "Désactiver les particularités de Lua 5.1",
|
||||
"gui.computercraft.config.disable_lua51_features.tooltip": "Définir sur true pour désactiver les fonctions Lua 5.1 qui seront supprimées dans une future mise à jour.\nUtile pour assurer la compatibilité ascendante de vos programmes actuels.",
|
||||
"gui.computercraft.config.disabled_generic_methods": "Méthodes génériques désactivées",
|
||||
"gui.computercraft.config.disabled_generic_methods.tooltip": "Une liste de méthodes génériques ou de méthodes source à désactiver.\nLes méthodes génériques sont ajoutées à un bloc ou bloc-entité lorsque aucune implémentation explicite de périphérique n'est disponible. Cela inclut les méthodes liées à l'inventaire (ex. inventory.getItemDetail, inventory.pushItems) et, avec Forge, les méthodes fluid_storage et energy_storage.\nLes méthodes de cette liste peuvent être groupées (computercraft:inventory) ou individuelles (computercraft:inventory#pushItems).\n",
|
||||
"gui.computercraft.config.execution": "Exécution",
|
||||
"gui.computercraft.config.execution.computer_threads": "Threads d'ordinateur",
|
||||
"gui.computercraft.config.execution.computer_threads.tooltip": "Définissez le nombre de threads sur lesquels les ordinateurs peuvent s'exécuter. Un nombre plus élevé signifie\nque plus d'ordinateurs peuvent fonctionner à la fois, mais peut induire un décalage. Veuillez noter que\ncertains mods peuvent ne pas fonctionner avec un nombre de threads supérieur à 1. À utiliser avec prudence.\nPlage : > 1",
|
||||
@@ -95,7 +94,7 @@
|
||||
"gui.computercraft.config.http.bandwidth.global_upload.tooltip": "Le nombre d'octets qui peuvent être téléversé en une seconde. Ceci est partagé sur tous les ordinateurs. (octets/s).\nPlage : > 1",
|
||||
"gui.computercraft.config.http.bandwidth.tooltip": "Limite la bande passante utilisée par les ordinateurs.",
|
||||
"gui.computercraft.config.http.enabled": "Permettre l'API HTTP",
|
||||
"gui.computercraft.config.http.enabled.tooltip": "Active l'API \"http\" sur les ordinateurs. Cela désactive également les programmes \"pastebin\" et \"wget\",\nsur lesquels de nombreux utilisateurs comptent. Il est recommandé de laisser cette option activée et\nd'utiliser l'option de configuration \"rules\" pour imposer un contrôle plus précis.",
|
||||
"gui.computercraft.config.http.enabled.tooltip": "Active l'API \"http\" sur les ordinateurs. La désactiver empêche également l'utilisation des programmes \"pastebin\" et \"wget\",\nsur lesquels de nombreux utilisateurs comptent. Il est recommandé de laisser cette option activée et\nd'utiliser l'option de configuration \"rules\" pour imposer un contrôle plus précis.",
|
||||
"gui.computercraft.config.http.max_requests": "Maximum de requêtes simultanées",
|
||||
"gui.computercraft.config.http.max_requests.tooltip": "Le nombre de requêtes http qu'un ordinateur peut effectuer en même temps.\nLes demandes supplémentaires seront mises en file d'attente et envoyées lorsque\nles demandes en cours seront terminées. Mettre à 0 pour illimité.\nPlage : > 0",
|
||||
"gui.computercraft.config.http.max_websockets": "Maximum de websockets en simultané",
|
||||
@@ -200,6 +199,8 @@
|
||||
"item.computercraft.printed_pages": "Pages imprimées",
|
||||
"item.computercraft.treasure_disk": "Disquette",
|
||||
"itemGroup.computercraft": "ComputerCraft",
|
||||
"tag.item.computercraft.computer": "Ordinateurs",
|
||||
"tag.item.computercraft.wired_modem": "Modems câblés",
|
||||
"tracking_field.computercraft.avg": "%s (moyenne)",
|
||||
"tracking_field.computercraft.computer_tasks.name": "Tâches",
|
||||
"tracking_field.computercraft.count": "%s (compte)",
|
||||
|
@@ -74,8 +74,6 @@
|
||||
"gui.computercraft.config.computer_space_limit.tooltip": "Limite di spazio di archiviazione per i computer e le tartarughe, in byte.",
|
||||
"gui.computercraft.config.default_computer_settings": "Impostazioni Computer predefinite",
|
||||
"gui.computercraft.config.default_computer_settings.tooltip": "Una lista di impostazioni predefinite per i nuovi computer, separate da virgola.\nEsempio: \"shell.autocomplete=false,lua.autocomplete=false,edit.autocomplete=false\"\ndisattiverà tutti gli autocompletamenti.",
|
||||
"gui.computercraft.config.disable_lua51_features": "Disattiva features Lua 5.1",
|
||||
"gui.computercraft.config.disable_lua51_features.tooltip": "Imposta a \"true\" per disattivare le funzionalità di Lua 5.1 che saranno rimosse in un\naggiornamento futuro. Utile per assicurare futura compatibilità con i tuoi programmi.",
|
||||
"gui.computercraft.config.execution": "Esecuzione",
|
||||
"gui.computercraft.config.execution.computer_threads": "Threads computer",
|
||||
"gui.computercraft.config.execution.computer_threads.tooltip": "Imposta la quantità di thread che possono eseguire i computer. Un numero più alto significa\nche più computer possono essere eseguiti alla volta, ma può indurre a lag. Alcune mod potrebbero\nnon funzionare con numeri di thread maggiore a 1. Usare con cautela.\nRange: > 1",
|
||||
|
@@ -70,7 +70,6 @@
|
||||
"commands.computercraft.view.synopsis": "컴퓨터의 터미널을 보기",
|
||||
"gui.computercraft.config.computer_space_limit": "컴퓨터 공간 제한 (바이트)",
|
||||
"gui.computercraft.config.default_computer_settings": "기본 컴퓨터 설정",
|
||||
"gui.computercraft.config.disable_lua51_features": "Lua 5.1 기능 미사용",
|
||||
"gui.computercraft.config.execution": "실행",
|
||||
"gui.computercraft.config.execution.computer_threads": "컴퓨터 쓰레드",
|
||||
"gui.computercraft.config.execution.max_main_computer_time": "컴퓨터 시간 당 서버 제한",
|
||||
|
@@ -38,7 +38,6 @@
|
||||
"commands.computercraft.turn_on.synopsis": "Liga computadores remotamente.",
|
||||
"gui.computercraft.config.computer_space_limit": "Limite de espaço dos Computadores (bytes)",
|
||||
"gui.computercraft.config.default_computer_settings": "Configurações padrão para Computadores",
|
||||
"gui.computercraft.config.disable_lua51_features": "Desabilitar funcionalidade da Lua 5.1",
|
||||
"gui.computercraft.config.execution.computer_threads": "Threads por computador",
|
||||
"gui.computercraft.config.floppy_space_limit": "Limite de espaço dos Disquetes (bytes)",
|
||||
"gui.computercraft.config.http": "HTTP",
|
||||
|
@@ -74,8 +74,6 @@
|
||||
"gui.computercraft.config.computer_space_limit.tooltip": "Лимит места на дисках компьютеров и черепашек, в байтах.",
|
||||
"gui.computercraft.config.default_computer_settings": "Настройки Компьютера по умолчанию",
|
||||
"gui.computercraft.config.default_computer_settings.tooltip": "Разделенный запятыми список системных настроек по умолчанию на новых компьютерах.\nНапример: \"shell.autocomplete=false,lua.autocomplete=false,edit.autocomplete=false\"\nотключит всё автодополнение.",
|
||||
"gui.computercraft.config.disable_lua51_features": "Отключить функции Lua 5.1",
|
||||
"gui.computercraft.config.disable_lua51_features.tooltip": "Поставьте, чтобы отключить функции из Lua 5.1, которые будут убраны в будущих\nобновлениях. Полезно для того, чтобы улучшить совместимость вперед ваших программ.",
|
||||
"gui.computercraft.config.execution": "Выполнение",
|
||||
"gui.computercraft.config.execution.computer_threads": "Потоки компьютера",
|
||||
"gui.computercraft.config.execution.computer_threads.tooltip": "Устанавливает количество потоков, на которых работают компьютеры. Большее число\nозначает, что больше компьютеров сможет работать одновременно, но может привести к лагу.\nОбратите внимание, что некоторые моды могут не работать с более чем одним потоком. Используйте с осторожностью.\nОграничение: > 1",
|
||||
|
@@ -70,7 +70,6 @@
|
||||
"commands.computercraft.view.synopsis": "Titta på datorns terminal.",
|
||||
"gui.computercraft.config.computer_space_limit": "Dator maximalt utrymme (bytes)",
|
||||
"gui.computercraft.config.default_computer_settings": "Standard Datorinställningar",
|
||||
"gui.computercraft.config.disable_lua51_features": "Avaktivera Lua 5.1 funktioner",
|
||||
"gui.computercraft.config.execution.computer_threads": "Dator trådar",
|
||||
"gui.computercraft.config.floppy_space_limit": "Diskett maximalt utrymme (bytes)",
|
||||
"gui.computercraft.config.http": "HTTP",
|
||||
|
@@ -66,7 +66,6 @@
|
||||
"commands.computercraft.view.not_player": "jan ala la, mi ken ala open e sitelen pi ilo sona",
|
||||
"commands.computercraft.view.synopsis": "o lukin e sitelen pi ilo sona.",
|
||||
"gui.computercraft.config.command_require_creative": "ilo sona pi toki wawa li wile e jan sewi",
|
||||
"gui.computercraft.config.disable_lua51_features": "ijo pona pi ilo Lua 5.1 li weka",
|
||||
"gui.computercraft.config.execution": "pali",
|
||||
"gui.computercraft.config.execution.computer_threads": "linja pi ilo sona",
|
||||
"gui.computercraft.config.http": "ijo HTTP",
|
||||
|
@@ -74,8 +74,6 @@
|
||||
"gui.computercraft.config.computer_space_limit.tooltip": "Обмеження на займаєме місце на диску комп'ютерами та черепахами, в байтах.",
|
||||
"gui.computercraft.config.default_computer_settings": "Налаштування комп'ютера за замовчуванням",
|
||||
"gui.computercraft.config.default_computer_settings.tooltip": "Список лашатувань за замовчуванням, розділених комою, що будуть нашалтовані на нових комп'ютерах\nНаприклад: \"shell.autocomplete=false,lua.autocomplete=false,edit.autocomplete=false\"\nвідключить усі автодоповнення.",
|
||||
"gui.computercraft.config.disable_lua51_features": "Відключити підтримку Lua 5.1",
|
||||
"gui.computercraft.config.disable_lua51_features.tooltip": "Включить це налаштування, якщо хочете відключити функціональність, пов'язану з Lua 5.1, яка буде вилучен в наступних оновленнях.\nТаким чином ви можете впевнитись, що ваші програми не зламаються від подальших оновлень.",
|
||||
"gui.computercraft.config.execution": "Виконання",
|
||||
"gui.computercraft.config.execution.computer_threads": "Потоки для комп'ютерів",
|
||||
"gui.computercraft.config.execution.computer_threads.tooltip": "Встановлює кількість потоків, на яких запускаються комп'ютери. Більше число\nозначає більшу кількість комп'ютерів, які працюють паралельно, але може призвести до проблем із продуктивністю.\nЗауважте, що деякі модифікації можуть не працювати із кількістю потоків більше за 1. Використовуйте з обережністю.\nОбмеження: > 1",
|
||||
|
@@ -68,7 +68,6 @@
|
||||
"commands.computercraft.view.synopsis": "查看计算机的终端.",
|
||||
"gui.computercraft.config.computer_space_limit": "计算机空间限制(字节)",
|
||||
"gui.computercraft.config.default_computer_settings": "默认计算机设置",
|
||||
"gui.computercraft.config.disable_lua51_features": "禁用Lua 5.1功能",
|
||||
"gui.computercraft.config.execution": "执行",
|
||||
"gui.computercraft.config.execution.computer_threads": "计算机线程数",
|
||||
"gui.computercraft.config.execution.max_main_computer_time": "服务器计算机tick时间限制",
|
||||
|
@@ -4,8 +4,6 @@
|
||||
|
||||
package dan200.computercraft.impl.network.wired;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
import dan200.computercraft.api.network.wired.WiredElement;
|
||||
import dan200.computercraft.api.network.wired.WiredNetwork;
|
||||
import dan200.computercraft.api.network.wired.WiredNetworkChange;
|
||||
@@ -19,6 +17,7 @@ import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
@@ -47,24 +46,24 @@ public class NetworkTest {
|
||||
assertFalse(aN.getNetwork().connect(aN, bN), "Cannot add connection twice");
|
||||
|
||||
assertEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must be equal");
|
||||
assertEquals(Sets.newHashSet(aN, bN), nodes(aN.getNetwork()), "A's network should be A and B");
|
||||
assertEquals(Set.of(aN, bN), nodes(aN.getNetwork()), "A's network should be A and B");
|
||||
|
||||
assertEquals(Sets.newHashSet("a", "b"), aE.allPeripherals().keySet(), "A's peripheral set should be A, B");
|
||||
assertEquals(Sets.newHashSet("a", "b"), bE.allPeripherals().keySet(), "B's peripheral set should be A, B");
|
||||
assertEquals(Set.of("a", "b"), aE.allPeripherals().keySet(), "A's peripheral set should be A, B");
|
||||
assertEquals(Set.of("a", "b"), bE.allPeripherals().keySet(), "B's peripheral set should be A, B");
|
||||
|
||||
aN.getNetwork().connect(aN, cN);
|
||||
|
||||
assertEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must be equal");
|
||||
assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal");
|
||||
assertEquals(Sets.newHashSet(aN, bN, cN), nodes(aN.getNetwork()), "A's network should be A, B and C");
|
||||
assertEquals(Set.of(aN, bN, cN), nodes(aN.getNetwork()), "A's network should be A, B and C");
|
||||
|
||||
assertEquals(Sets.newHashSet(bN, cN), neighbours(aN), "A's neighbour set should be B, C");
|
||||
assertEquals(Sets.newHashSet(aN), neighbours(bN), "B's neighbour set should be A");
|
||||
assertEquals(Sets.newHashSet(aN), neighbours(cN), "C's neighbour set should be A");
|
||||
assertEquals(Set.of(bN, cN), neighbours(aN), "A's neighbour set should be B, C");
|
||||
assertEquals(Set.of(aN), neighbours(bN), "B's neighbour set should be A");
|
||||
assertEquals(Set.of(aN), neighbours(cN), "C's neighbour set should be A");
|
||||
|
||||
assertEquals(Sets.newHashSet("a", "b", "c"), aE.allPeripherals().keySet(), "A's peripheral set should be A, B, C");
|
||||
assertEquals(Sets.newHashSet("a", "b", "c"), bE.allPeripherals().keySet(), "B's peripheral set should be A, B, C");
|
||||
assertEquals(Sets.newHashSet("a", "b", "c"), cE.allPeripherals().keySet(), "C's peripheral set should be A, B, C");
|
||||
assertEquals(Set.of("a", "b", "c"), aE.allPeripherals().keySet(), "A's peripheral set should be A, B, C");
|
||||
assertEquals(Set.of("a", "b", "c"), bE.allPeripherals().keySet(), "B's peripheral set should be A, B, C");
|
||||
assertEquals(Set.of("a", "b", "c"), cE.allPeripherals().keySet(), "C's peripheral set should be A, B, C");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -87,11 +86,11 @@ public class NetworkTest {
|
||||
|
||||
assertEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must be equal");
|
||||
assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal");
|
||||
assertEquals(Sets.newHashSet(aN, bN, cN), nodes(aN.getNetwork()), "A's network should be A, B and C");
|
||||
assertEquals(Set.of(aN, bN, cN), nodes(aN.getNetwork()), "A's network should be A, B and C");
|
||||
|
||||
assertEquals(Sets.newHashSet("a", "b", "c"), aE.allPeripherals().keySet(), "A's peripheral set should be A, B, C");
|
||||
assertEquals(Sets.newHashSet("a", "b", "c"), bE.allPeripherals().keySet(), "B's peripheral set should be A, B, C");
|
||||
assertEquals(Sets.newHashSet("a", "b", "c"), cE.allPeripherals().keySet(), "C's peripheral set should be A, B, C");
|
||||
assertEquals(Set.of("a", "b", "c"), aE.allPeripherals().keySet(), "A's peripheral set should be A, B, C");
|
||||
assertEquals(Set.of("a", "b", "c"), bE.allPeripherals().keySet(), "B's peripheral set should be A, B, C");
|
||||
assertEquals(Set.of("a", "b", "c"), cE.allPeripherals().keySet(), "C's peripheral set should be A, B, C");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -113,12 +112,12 @@ public class NetworkTest {
|
||||
|
||||
assertNotEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal");
|
||||
assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal");
|
||||
assertEquals(Sets.newHashSet(aN, cN), nodes(aN.getNetwork()), "A's network should be A and C");
|
||||
assertEquals(Sets.newHashSet(bN), nodes(bN.getNetwork()), "B's network should be B");
|
||||
assertEquals(Set.of(aN, cN), nodes(aN.getNetwork()), "A's network should be A and C");
|
||||
assertEquals(Set.of(bN), nodes(bN.getNetwork()), "B's network should be B");
|
||||
|
||||
assertEquals(Sets.newHashSet("a", "c"), aE.allPeripherals().keySet(), "A's peripheral set should be A, C");
|
||||
assertEquals(Sets.newHashSet("b"), bE.allPeripherals().keySet(), "B's peripheral set should be B");
|
||||
assertEquals(Sets.newHashSet("a", "c"), cE.allPeripherals().keySet(), "C's peripheral set should be A, C");
|
||||
assertEquals(Set.of("a", "c"), aE.allPeripherals().keySet(), "A's peripheral set should be A, C");
|
||||
assertEquals(Set.of("b"), bE.allPeripherals().keySet(), "B's peripheral set should be B");
|
||||
assertEquals(Set.of("a", "c"), cE.allPeripherals().keySet(), "C's peripheral set should be A, C");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -146,11 +145,11 @@ public class NetworkTest {
|
||||
assertEquals(aN.getNetwork(), aaN.getNetwork(), "A's and A_'s network must be equal");
|
||||
assertEquals(bN.getNetwork(), bbN.getNetwork(), "B's and B_'s network must be equal");
|
||||
|
||||
assertEquals(Sets.newHashSet(aN, aaN), nodes(aN.getNetwork()), "A's network should be A and A_");
|
||||
assertEquals(Sets.newHashSet(bN, bbN), nodes(bN.getNetwork()), "B's network should be B and B_");
|
||||
assertEquals(Set.of(aN, aaN), nodes(aN.getNetwork()), "A's network should be A and A_");
|
||||
assertEquals(Set.of(bN, bbN), nodes(bN.getNetwork()), "B's network should be B and B_");
|
||||
|
||||
assertEquals(Sets.newHashSet("a", "a_"), aE.allPeripherals().keySet(), "A's peripheral set should be A and A_");
|
||||
assertEquals(Sets.newHashSet("b", "b_"), bE.allPeripherals().keySet(), "B's peripheral set should be B and B_");
|
||||
assertEquals(Set.of("a", "a_"), aE.allPeripherals().keySet(), "A's peripheral set should be A and A_");
|
||||
assertEquals(Set.of("b", "b_"), bE.allPeripherals().keySet(), "B's peripheral set should be B and B_");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -184,12 +183,12 @@ public class NetworkTest {
|
||||
assertNotEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal");
|
||||
assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal");
|
||||
|
||||
assertEquals(Sets.newHashSet(aN, cN), nodes(aN.getNetwork()), "A's network should be A and C");
|
||||
assertEquals(Sets.newHashSet(bN), nodes(bN.getNetwork()), "B's network should be B");
|
||||
assertEquals(Set.of(aN, cN), nodes(aN.getNetwork()), "A's network should be A and C");
|
||||
assertEquals(Set.of(bN), nodes(bN.getNetwork()), "B's network should be B");
|
||||
|
||||
assertEquals(Sets.newHashSet("a", "c"), aE.allPeripherals().keySet(), "A's peripheral set should be A, C");
|
||||
assertEquals(Sets.newHashSet(), bE.allPeripherals().keySet(), "B's peripheral set should be empty");
|
||||
assertEquals(Sets.newHashSet("a", "c"), cE.allPeripherals().keySet(), "C's peripheral set should be A, C");
|
||||
assertEquals(Set.of("a", "c"), aE.allPeripherals().keySet(), "A's peripheral set should be A, C");
|
||||
assertEquals(Set.of(), bE.allPeripherals().keySet(), "B's peripheral set should be empty");
|
||||
assertEquals(Set.of("a", "c"), cE.allPeripherals().keySet(), "C's peripheral set should be A, C");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -220,13 +219,13 @@ public class NetworkTest {
|
||||
assertEquals(aN.getNetwork(), aaN.getNetwork(), "A's and A_'s network must be equal");
|
||||
assertEquals(bN.getNetwork(), bbN.getNetwork(), "B's and B_'s network must be equal");
|
||||
|
||||
assertEquals(Sets.newHashSet(aN, aaN), nodes(aN.getNetwork()), "A's network should be A and A_");
|
||||
assertEquals(Sets.newHashSet(bN, bbN), nodes(bN.getNetwork()), "B's network should be B and B_");
|
||||
assertEquals(Sets.newHashSet(cN), nodes(cN.getNetwork()), "C's network should be C");
|
||||
assertEquals(Set.of(aN, aaN), nodes(aN.getNetwork()), "A's network should be A and A_");
|
||||
assertEquals(Set.of(bN, bbN), nodes(bN.getNetwork()), "B's network should be B and B_");
|
||||
assertEquals(Set.of(cN), nodes(cN.getNetwork()), "C's network should be C");
|
||||
|
||||
assertEquals(Sets.newHashSet("a", "a_"), aE.allPeripherals().keySet(), "A's peripheral set should be A and A_");
|
||||
assertEquals(Sets.newHashSet("b", "b_"), bE.allPeripherals().keySet(), "B's peripheral set should be B and B_");
|
||||
assertEquals(Sets.newHashSet(), cE.allPeripherals().keySet(), "C's peripheral set should be empty");
|
||||
assertEquals(Set.of("a", "a_"), aE.allPeripherals().keySet(), "A's peripheral set should be A and A_");
|
||||
assertEquals(Set.of("b", "b_"), bE.allPeripherals().keySet(), "B's peripheral set should be B and B_");
|
||||
assertEquals(Set.of(), cE.allPeripherals().keySet(), "C's peripheral set should be empty");
|
||||
}
|
||||
|
||||
private static final int BRUTE_SIZE = 16;
|
||||
@@ -300,8 +299,8 @@ public class NetworkTest {
|
||||
private final Vec3 position;
|
||||
private final String id;
|
||||
private final WiredNode node;
|
||||
private final Map<String, IPeripheral> localPeripherals = Maps.newHashMap();
|
||||
private final Map<String, IPeripheral> remotePeripherals = Maps.newHashMap();
|
||||
private final Map<String, IPeripheral> localPeripherals = new HashMap<>();
|
||||
private final Map<String, IPeripheral> remotePeripherals = new HashMap<>();
|
||||
|
||||
private NetworkElement(Level world, Vec3 position, String id) {
|
||||
this.world = world;
|
||||
|
@@ -16,7 +16,6 @@ import org.hamcrest.Matcher;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -110,7 +109,7 @@ public class UploadFileMessageTest {
|
||||
@Provide
|
||||
Arbitrary<FileUpload> fileUpload() {
|
||||
return Combinators.combine(
|
||||
Arbitraries.oneOf(Arrays.asList(
|
||||
Arbitraries.oneOf(List.of(
|
||||
// 1.16 doesn't correctly handle unicode file names. We'll be generous in our tests here.
|
||||
Arbitraries.strings().ofMinLength(1).ascii().ofMaxLength(MAX_FILE_NAME),
|
||||
Arbitraries.strings().ofMinLength(1).ofMaxLength(MAX_FILE_NAME / 4)
|
||||
|
@@ -52,8 +52,8 @@ public class JsonDump {
|
||||
inputs[pos] = itemIds;
|
||||
}
|
||||
|
||||
private static final Set<Item> canonicalItem = new HashSet<>(Arrays.asList(
|
||||
private static final Set<Item> canonicalItem = Set.of(
|
||||
Items.GLASS_PANE, Items.STONE, Items.CHEST
|
||||
));
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -17,7 +17,10 @@ import net.minecraft.gametest.framework.GameTestGenerator
|
||||
import net.minecraft.gametest.framework.GameTestHelper
|
||||
import net.minecraft.gametest.framework.TestFunction
|
||||
import net.minecraft.nbt.CompoundTag
|
||||
import net.minecraft.world.entity.EntityType
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.level.block.Blocks
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import java.util.*
|
||||
|
||||
class Monitor_Test {
|
||||
@@ -32,7 +35,7 @@ class Monitor_Test {
|
||||
|
||||
val toSet = BlockInput(
|
||||
ModRegistry.Blocks.MONITOR_ADVANCED.get().defaultBlockState(),
|
||||
Collections.emptySet(),
|
||||
emptySet(),
|
||||
tag,
|
||||
)
|
||||
|
||||
@@ -61,6 +64,45 @@ class Monitor_Test {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When a monitor is destroyed and then replaced, the terminal is recreated.
|
||||
*/
|
||||
@GameTest
|
||||
fun Creates_terminal(helper: GameTestHelper) = helper.sequence {
|
||||
fun monitorAt(x: Int) =
|
||||
helper.getBlockEntity(BlockPos(x, 2, 2), ModRegistry.BlockEntities.MONITOR_ADVANCED.get())
|
||||
|
||||
thenExecute {
|
||||
for (i in 1..3) {
|
||||
assertNull(monitorAt(i).cachedServerMonitor, "Monitor $i starts with no ServerMonitor")
|
||||
}
|
||||
|
||||
monitorAt(2).peripheral()
|
||||
assertNotNull(monitorAt(1).cachedServerMonitor?.terminal, "Creating a peripheral creates a terminal")
|
||||
|
||||
// Then remove the middle monitor and check it splits into two.
|
||||
helper.setBlock(BlockPos(2, 2, 2), Blocks.AIR.defaultBlockState())
|
||||
|
||||
assertNotNull(monitorAt(3).cachedServerMonitor, "Origin retains its monitor")
|
||||
assertNull(monitorAt(3).cachedServerMonitor!!.terminal, "Origin deletes the terminal")
|
||||
assertNotEquals(monitorAt(1).cachedServerMonitor, monitorAt(3).cachedServerMonitor, "Monitors are different")
|
||||
|
||||
// Then set the monitor, check it rejoins and recreates the terminal.
|
||||
val pos = BlockPos(2, 2, 2)
|
||||
helper.setBlock(pos, ModRegistry.Blocks.MONITOR_ADVANCED.get())
|
||||
ModRegistry.Blocks.MONITOR_ADVANCED.get().setPlacedBy(
|
||||
helper.level,
|
||||
helper.absolutePos(pos),
|
||||
helper.getBlockState(pos),
|
||||
EntityType.COW.create(helper.level),
|
||||
ItemStack.EMPTY,
|
||||
)
|
||||
monitorAt(2).peripheral()
|
||||
|
||||
assertNotNull(monitorAt(1).cachedServerMonitor?.terminal, "Recreates the terminal")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test monitors render correctly
|
||||
*/
|
||||
|
@@ -37,6 +37,7 @@ import net.minecraft.world.item.Items
|
||||
import net.minecraft.world.item.enchantment.Enchantments
|
||||
import net.minecraft.world.level.block.Blocks
|
||||
import net.minecraft.world.level.block.FenceBlock
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.array
|
||||
@@ -86,6 +87,26 @@ class Turtle_Test {
|
||||
thenExecute { helper.assertBlockPresent(Blocks.LAVA, BlockPos(2, 2, 2)) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks turtles can write to signs.
|
||||
*
|
||||
* @see [#1611](https://github.com/cc-tweaked/CC-Tweaked/issues/1611)
|
||||
*/
|
||||
@GameTest
|
||||
fun Place_sign(helper: GameTestHelper) = helper.sequence {
|
||||
thenOnComputer {
|
||||
turtle.place(ObjectArguments("Test\nmessage")).await()
|
||||
.assertArrayEquals(true, message = "Placed sign")
|
||||
}
|
||||
thenExecute {
|
||||
val sign = helper.getBlockEntity(BlockPos(2, 2, 1), BlockEntityType.SIGN)
|
||||
val lines = listOf("", "Test", "message", "")
|
||||
for ((i, line) in lines.withIndex()) {
|
||||
assertEquals(line, sign.frontText.getMessage(i, false).string, "Line $i")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that calling [net.minecraft.world.item.Item.use] will not place blocks too far away.
|
||||
*
|
||||
|
@@ -17,7 +17,7 @@ import net.minecraft.gametest.framework.GameTestAssertException
|
||||
import net.minecraft.gametest.framework.GameTestAssertPosException
|
||||
import net.minecraft.gametest.framework.GameTestInfo
|
||||
import net.minecraft.gametest.framework.GameTestSequence
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
import java.util.concurrent.CancellationException
|
||||
@@ -33,7 +33,7 @@ import java.util.concurrent.atomic.AtomicReference
|
||||
* @see GameTestSequence.thenOnComputer
|
||||
*/
|
||||
object ManagedComputers : ILuaMachine.Factory {
|
||||
private val LOGGER = LogManager.getLogger(ManagedComputers::class.java)
|
||||
private val LOGGER = LoggerFactory.getLogger(ManagedComputers::class.java)
|
||||
private val computers: MutableMap<String, Queue<suspend LuaTaskContext.() -> Unit>> = mutableMapOf()
|
||||
|
||||
internal fun enqueue(test: GameTestInfo, label: String, task: suspend LuaTaskContext.() -> Unit): Monitor {
|
||||
|
139
projects/common/src/testMod/resources/data/cctest/structures/monitor_test.creates_terminal.snbt
generated
Normal file
139
projects/common/src/testMod/resources/data/cctest/structures/monitor_test.creates_terminal.snbt
generated
Normal file
@@ -0,0 +1,139 @@
|
||||
{
|
||||
DataVersion: 3465,
|
||||
size: [5, 5, 5],
|
||||
data: [
|
||||
{pos: [0, 0, 0], state: "minecraft:polished_andesite"},
|
||||
{pos: [0, 0, 1], state: "minecraft:polished_andesite"},
|
||||
{pos: [0, 0, 2], state: "minecraft:polished_andesite"},
|
||||
{pos: [0, 0, 3], state: "minecraft:polished_andesite"},
|
||||
{pos: [0, 0, 4], state: "minecraft:polished_andesite"},
|
||||
{pos: [1, 0, 0], state: "minecraft:polished_andesite"},
|
||||
{pos: [1, 0, 1], state: "minecraft:polished_andesite"},
|
||||
{pos: [1, 0, 2], state: "minecraft:polished_andesite"},
|
||||
{pos: [1, 0, 3], state: "minecraft:polished_andesite"},
|
||||
{pos: [1, 0, 4], state: "minecraft:polished_andesite"},
|
||||
{pos: [2, 0, 0], state: "minecraft:polished_andesite"},
|
||||
{pos: [2, 0, 1], state: "minecraft:polished_andesite"},
|
||||
{pos: [2, 0, 2], state: "minecraft:polished_andesite"},
|
||||
{pos: [2, 0, 3], state: "minecraft:polished_andesite"},
|
||||
{pos: [2, 0, 4], state: "minecraft:polished_andesite"},
|
||||
{pos: [3, 0, 0], state: "minecraft:polished_andesite"},
|
||||
{pos: [3, 0, 1], state: "minecraft:polished_andesite"},
|
||||
{pos: [3, 0, 2], state: "minecraft:polished_andesite"},
|
||||
{pos: [3, 0, 3], state: "minecraft:polished_andesite"},
|
||||
{pos: [3, 0, 4], state: "minecraft:polished_andesite"},
|
||||
{pos: [4, 0, 0], state: "minecraft:polished_andesite"},
|
||||
{pos: [4, 0, 1], state: "minecraft:polished_andesite"},
|
||||
{pos: [4, 0, 2], state: "minecraft:polished_andesite"},
|
||||
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
|
||||
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
|
||||
{pos: [0, 1, 0], state: "minecraft:air"},
|
||||
{pos: [0, 1, 1], state: "minecraft:air"},
|
||||
{pos: [0, 1, 2], state: "minecraft:air"},
|
||||
{pos: [0, 1, 3], state: "minecraft:air"},
|
||||
{pos: [0, 1, 4], state: "minecraft:air"},
|
||||
{pos: [1, 1, 0], state: "minecraft:air"},
|
||||
{pos: [1, 1, 1], state: "minecraft:air"},
|
||||
{pos: [1, 1, 2], state: "computercraft:monitor_advanced{facing:north,orientation:north,state:l}", nbt: {Height: 1, Width: 3, XIndex: 2, YIndex: 0, id: "computercraft:monitor_advanced"}},
|
||||
{pos: [1, 1, 3], state: "minecraft:air"},
|
||||
{pos: [1, 1, 4], state: "minecraft:air"},
|
||||
{pos: [2, 1, 0], state: "minecraft:air"},
|
||||
{pos: [2, 1, 1], state: "minecraft:air"},
|
||||
{pos: [2, 1, 2], state: "computercraft:monitor_advanced{facing:north,orientation:north,state:lr}", nbt: {Height: 1, Width: 3, XIndex: 1, YIndex: 0, id: "computercraft:monitor_advanced"}},
|
||||
{pos: [2, 1, 3], state: "minecraft:air"},
|
||||
{pos: [2, 1, 4], state: "minecraft:air"},
|
||||
{pos: [3, 1, 0], state: "minecraft:air"},
|
||||
{pos: [3, 1, 1], state: "minecraft:air"},
|
||||
{pos: [3, 1, 2], state: "computercraft:monitor_advanced{facing:north,orientation:north,state:r}", nbt: {Height: 1, Width: 3, XIndex: 0, YIndex: 0, id: "computercraft:monitor_advanced"}},
|
||||
{pos: [3, 1, 3], state: "minecraft:air"},
|
||||
{pos: [3, 1, 4], state: "minecraft:air"},
|
||||
{pos: [4, 1, 0], state: "minecraft:air"},
|
||||
{pos: [4, 1, 1], state: "minecraft:air"},
|
||||
{pos: [4, 1, 2], state: "minecraft:air"},
|
||||
{pos: [4, 1, 3], state: "minecraft:air"},
|
||||
{pos: [4, 1, 4], state: "minecraft:air"},
|
||||
{pos: [0, 3, 0], state: "minecraft:air"},
|
||||
{pos: [0, 2, 1], state: "minecraft:air"},
|
||||
{pos: [0, 2, 2], state: "minecraft:air"},
|
||||
{pos: [0, 2, 3], state: "minecraft:air"},
|
||||
{pos: [0, 2, 4], state: "minecraft:air"},
|
||||
{pos: [1, 2, 0], state: "minecraft:air"},
|
||||
{pos: [1, 2, 1], state: "minecraft:air"},
|
||||
{pos: [1, 2, 2], state: "minecraft:air"},
|
||||
{pos: [1, 2, 3], state: "minecraft:air"},
|
||||
{pos: [1, 2, 4], state: "minecraft:air"},
|
||||
{pos: [2, 2, 0], state: "minecraft:air"},
|
||||
{pos: [2, 2, 1], state: "minecraft:air"},
|
||||
{pos: [2, 2, 2], state: "minecraft:air"},
|
||||
{pos: [2, 2, 3], state: "minecraft:air"},
|
||||
{pos: [2, 2, 4], state: "minecraft:air"},
|
||||
{pos: [3, 2, 0], state: "minecraft:air"},
|
||||
{pos: [3, 2, 1], state: "minecraft:air"},
|
||||
{pos: [3, 2, 2], state: "minecraft:air"},
|
||||
{pos: [3, 2, 3], state: "minecraft:air"},
|
||||
{pos: [3, 2, 4], state: "minecraft:air"},
|
||||
{pos: [4, 2, 0], state: "minecraft:air"},
|
||||
{pos: [4, 2, 1], state: "minecraft:air"},
|
||||
{pos: [4, 2, 2], state: "minecraft:air"},
|
||||
{pos: [4, 2, 3], state: "minecraft:air"},
|
||||
{pos: [4, 2, 4], state: "minecraft:air"},
|
||||
{pos: [0, 3, 0], state: "minecraft:air"},
|
||||
{pos: [0, 3, 1], state: "minecraft:air"},
|
||||
{pos: [0, 3, 2], state: "minecraft:air"},
|
||||
{pos: [0, 3, 3], state: "minecraft:air"},
|
||||
{pos: [0, 3, 4], state: "minecraft:air"},
|
||||
{pos: [1, 3, 0], state: "minecraft:air"},
|
||||
{pos: [1, 3, 1], state: "minecraft:air"},
|
||||
{pos: [1, 3, 2], state: "minecraft:air"},
|
||||
{pos: [1, 3, 3], state: "minecraft:air"},
|
||||
{pos: [1, 3, 4], state: "minecraft:air"},
|
||||
{pos: [2, 3, 0], state: "minecraft:air"},
|
||||
{pos: [2, 3, 1], state: "minecraft:air"},
|
||||
{pos: [2, 3, 2], state: "minecraft:air"},
|
||||
{pos: [2, 3, 3], state: "minecraft:air"},
|
||||
{pos: [2, 3, 4], state: "minecraft:air"},
|
||||
{pos: [3, 3, 0], state: "minecraft:air"},
|
||||
{pos: [3, 3, 1], state: "minecraft:air"},
|
||||
{pos: [3, 3, 2], state: "minecraft:air"},
|
||||
{pos: [3, 3, 3], state: "minecraft:air"},
|
||||
{pos: [3, 3, 4], state: "minecraft:air"},
|
||||
{pos: [4, 3, 0], state: "minecraft:air"},
|
||||
{pos: [4, 3, 1], state: "minecraft:air"},
|
||||
{pos: [4, 3, 2], state: "minecraft:air"},
|
||||
{pos: [4, 3, 3], state: "minecraft:air"},
|
||||
{pos: [4, 3, 4], state: "minecraft:air"},
|
||||
{pos: [0, 4, 0], state: "minecraft:air"},
|
||||
{pos: [0, 4, 1], state: "minecraft:air"},
|
||||
{pos: [0, 4, 2], state: "minecraft:air"},
|
||||
{pos: [0, 4, 3], state: "minecraft:air"},
|
||||
{pos: [0, 4, 4], state: "minecraft:air"},
|
||||
{pos: [1, 4, 0], state: "minecraft:air"},
|
||||
{pos: [1, 4, 1], state: "minecraft:air"},
|
||||
{pos: [1, 4, 2], state: "minecraft:air"},
|
||||
{pos: [1, 4, 3], state: "minecraft:air"},
|
||||
{pos: [1, 4, 4], state: "minecraft:air"},
|
||||
{pos: [2, 4, 0], state: "minecraft:air"},
|
||||
{pos: [2, 4, 1], state: "minecraft:air"},
|
||||
{pos: [2, 4, 2], state: "minecraft:air"},
|
||||
{pos: [2, 4, 3], state: "minecraft:air"},
|
||||
{pos: [2, 4, 4], state: "minecraft:air"},
|
||||
{pos: [3, 4, 0], state: "minecraft:air"},
|
||||
{pos: [3, 4, 1], state: "minecraft:air"},
|
||||
{pos: [3, 4, 2], state: "minecraft:air"},
|
||||
{pos: [3, 4, 3], state: "minecraft:air"},
|
||||
{pos: [3, 4, 4], state: "minecraft:air"},
|
||||
{pos: [4, 4, 0], state: "minecraft:air"},
|
||||
{pos: [4, 4, 1], state: "minecraft:air"},
|
||||
{pos: [4, 4, 2], state: "minecraft:air"},
|
||||
{pos: [4, 4, 3], state: "minecraft:air"},
|
||||
{pos: [4, 4, 4], state: "minecraft:air"}
|
||||
],
|
||||
entities: [],
|
||||
palette: [
|
||||
"minecraft:polished_andesite",
|
||||
"minecraft:air",
|
||||
"computercraft:monitor_advanced{facing:north,orientation:north,state:l}",
|
||||
"computercraft:monitor_advanced{facing:north,orientation:north,state:lr}",
|
||||
"computercraft:monitor_advanced{facing:north,orientation:north,state:r}"
|
||||
]
|
||||
}
|
137
projects/common/src/testMod/resources/data/cctest/structures/turtle_test.place_sign.snbt
generated
Normal file
137
projects/common/src/testMod/resources/data/cctest/structures/turtle_test.place_sign.snbt
generated
Normal file
@@ -0,0 +1,137 @@
|
||||
{
|
||||
DataVersion: 3465,
|
||||
size: [5, 5, 5],
|
||||
data: [
|
||||
{pos: [0, 0, 0], state: "minecraft:polished_andesite"},
|
||||
{pos: [0, 0, 1], state: "minecraft:polished_andesite"},
|
||||
{pos: [0, 0, 2], state: "minecraft:polished_andesite"},
|
||||
{pos: [0, 0, 3], state: "minecraft:polished_andesite"},
|
||||
{pos: [0, 0, 4], state: "minecraft:polished_andesite"},
|
||||
{pos: [1, 0, 0], state: "minecraft:polished_andesite"},
|
||||
{pos: [1, 0, 1], state: "minecraft:polished_andesite"},
|
||||
{pos: [1, 0, 2], state: "minecraft:polished_andesite"},
|
||||
{pos: [1, 0, 3], state: "minecraft:polished_andesite"},
|
||||
{pos: [1, 0, 4], state: "minecraft:polished_andesite"},
|
||||
{pos: [2, 0, 0], state: "minecraft:polished_andesite"},
|
||||
{pos: [2, 0, 1], state: "minecraft:polished_andesite"},
|
||||
{pos: [2, 0, 2], state: "minecraft:polished_andesite"},
|
||||
{pos: [2, 0, 3], state: "minecraft:polished_andesite"},
|
||||
{pos: [2, 0, 4], state: "minecraft:polished_andesite"},
|
||||
{pos: [3, 0, 0], state: "minecraft:polished_andesite"},
|
||||
{pos: [3, 0, 1], state: "minecraft:polished_andesite"},
|
||||
{pos: [3, 0, 2], state: "minecraft:polished_andesite"},
|
||||
{pos: [3, 0, 3], state: "minecraft:polished_andesite"},
|
||||
{pos: [3, 0, 4], state: "minecraft:polished_andesite"},
|
||||
{pos: [4, 0, 0], state: "minecraft:polished_andesite"},
|
||||
{pos: [4, 0, 1], state: "minecraft:polished_andesite"},
|
||||
{pos: [4, 0, 2], state: "minecraft:polished_andesite"},
|
||||
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
|
||||
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
|
||||
{pos: [0, 1, 0], state: "minecraft:air"},
|
||||
{pos: [0, 1, 1], state: "minecraft:air"},
|
||||
{pos: [0, 1, 2], state: "minecraft:air"},
|
||||
{pos: [0, 1, 3], state: "minecraft:air"},
|
||||
{pos: [0, 1, 4], state: "minecraft:air"},
|
||||
{pos: [1, 1, 0], state: "minecraft:air"},
|
||||
{pos: [1, 1, 1], state: "minecraft:air"},
|
||||
{pos: [1, 1, 2], state: "minecraft:air"},
|
||||
{pos: [1, 1, 3], state: "minecraft:air"},
|
||||
{pos: [1, 1, 4], state: "minecraft:air"},
|
||||
{pos: [2, 1, 0], state: "minecraft:air"},
|
||||
{pos: [2, 1, 1], state: "minecraft:air"},
|
||||
{pos: [2, 1, 2], state: "computercraft:turtle_normal{facing:north,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [{Count: 1b, Slot: 0b, id: "minecraft:oak_sign"}], Label: "turtle_test.place_sign", On: 1b, Owner: {LowerId: -7553144124013011039L, Name: "Player840", UpperId: 8473952144820417152L}, Slot: 0, id: "computercraft:turtle_normal"}},
|
||||
{pos: [2, 1, 3], state: "minecraft:air"},
|
||||
{pos: [2, 1, 4], state: "minecraft:air"},
|
||||
{pos: [3, 1, 0], state: "minecraft:air"},
|
||||
{pos: [3, 1, 1], state: "minecraft:air"},
|
||||
{pos: [3, 1, 2], state: "minecraft:air"},
|
||||
{pos: [3, 1, 3], state: "minecraft:air"},
|
||||
{pos: [3, 1, 4], state: "minecraft:air"},
|
||||
{pos: [4, 1, 0], state: "minecraft:air"},
|
||||
{pos: [4, 1, 1], state: "minecraft:air"},
|
||||
{pos: [4, 1, 2], state: "minecraft:air"},
|
||||
{pos: [4, 1, 3], state: "minecraft:air"},
|
||||
{pos: [4, 1, 4], state: "minecraft:air"},
|
||||
{pos: [0, 2, 0], state: "minecraft:air"},
|
||||
{pos: [0, 2, 1], state: "minecraft:air"},
|
||||
{pos: [0, 2, 2], state: "minecraft:air"},
|
||||
{pos: [0, 2, 3], state: "minecraft:air"},
|
||||
{pos: [0, 2, 4], state: "minecraft:air"},
|
||||
{pos: [1, 2, 0], state: "minecraft:air"},
|
||||
{pos: [1, 2, 1], state: "minecraft:air"},
|
||||
{pos: [1, 2, 2], state: "minecraft:air"},
|
||||
{pos: [1, 2, 3], state: "minecraft:air"},
|
||||
{pos: [1, 2, 4], state: "minecraft:air"},
|
||||
{pos: [2, 2, 0], state: "minecraft:air"},
|
||||
{pos: [2, 2, 1], state: "minecraft:air"},
|
||||
{pos: [2, 2, 2], state: "minecraft:air"},
|
||||
{pos: [2, 2, 3], state: "minecraft:air"},
|
||||
{pos: [2, 2, 4], state: "minecraft:air"},
|
||||
{pos: [3, 2, 0], state: "minecraft:air"},
|
||||
{pos: [3, 2, 1], state: "minecraft:air"},
|
||||
{pos: [3, 2, 2], state: "minecraft:air"},
|
||||
{pos: [3, 2, 3], state: "minecraft:air"},
|
||||
{pos: [3, 2, 4], state: "minecraft:air"},
|
||||
{pos: [4, 2, 0], state: "minecraft:air"},
|
||||
{pos: [4, 2, 1], state: "minecraft:air"},
|
||||
{pos: [4, 2, 2], state: "minecraft:air"},
|
||||
{pos: [4, 2, 3], state: "minecraft:air"},
|
||||
{pos: [4, 2, 4], state: "minecraft:air"},
|
||||
{pos: [0, 3, 0], state: "minecraft:air"},
|
||||
{pos: [0, 3, 1], state: "minecraft:air"},
|
||||
{pos: [0, 3, 2], state: "minecraft:air"},
|
||||
{pos: [0, 3, 3], state: "minecraft:air"},
|
||||
{pos: [0, 3, 4], state: "minecraft:air"},
|
||||
{pos: [1, 3, 0], state: "minecraft:air"},
|
||||
{pos: [1, 3, 1], state: "minecraft:air"},
|
||||
{pos: [1, 3, 2], state: "minecraft:air"},
|
||||
{pos: [1, 3, 3], state: "minecraft:air"},
|
||||
{pos: [1, 3, 4], state: "minecraft:air"},
|
||||
{pos: [2, 3, 0], state: "minecraft:air"},
|
||||
{pos: [2, 3, 1], state: "minecraft:air"},
|
||||
{pos: [2, 3, 2], state: "minecraft:air"},
|
||||
{pos: [2, 3, 3], state: "minecraft:air"},
|
||||
{pos: [2, 3, 4], state: "minecraft:air"},
|
||||
{pos: [3, 3, 0], state: "minecraft:air"},
|
||||
{pos: [3, 3, 1], state: "minecraft:air"},
|
||||
{pos: [3, 3, 2], state: "minecraft:air"},
|
||||
{pos: [3, 3, 3], state: "minecraft:air"},
|
||||
{pos: [3, 3, 4], state: "minecraft:air"},
|
||||
{pos: [4, 3, 0], state: "minecraft:air"},
|
||||
{pos: [4, 3, 1], state: "minecraft:air"},
|
||||
{pos: [4, 3, 2], state: "minecraft:air"},
|
||||
{pos: [4, 3, 3], state: "minecraft:air"},
|
||||
{pos: [4, 3, 4], state: "minecraft:air"},
|
||||
{pos: [0, 4, 0], state: "minecraft:air"},
|
||||
{pos: [0, 4, 1], state: "minecraft:air"},
|
||||
{pos: [0, 4, 2], state: "minecraft:air"},
|
||||
{pos: [0, 4, 3], state: "minecraft:air"},
|
||||
{pos: [0, 4, 4], state: "minecraft:air"},
|
||||
{pos: [1, 4, 0], state: "minecraft:air"},
|
||||
{pos: [1, 4, 1], state: "minecraft:air"},
|
||||
{pos: [1, 4, 2], state: "minecraft:air"},
|
||||
{pos: [1, 4, 3], state: "minecraft:air"},
|
||||
{pos: [1, 4, 4], state: "minecraft:air"},
|
||||
{pos: [2, 4, 0], state: "minecraft:air"},
|
||||
{pos: [2, 4, 1], state: "minecraft:air"},
|
||||
{pos: [2, 4, 2], state: "minecraft:air"},
|
||||
{pos: [2, 4, 3], state: "minecraft:air"},
|
||||
{pos: [2, 4, 4], state: "minecraft:air"},
|
||||
{pos: [3, 4, 0], state: "minecraft:air"},
|
||||
{pos: [3, 4, 1], state: "minecraft:air"},
|
||||
{pos: [3, 4, 2], state: "minecraft:air"},
|
||||
{pos: [3, 4, 3], state: "minecraft:air"},
|
||||
{pos: [3, 4, 4], state: "minecraft:air"},
|
||||
{pos: [4, 4, 0], state: "minecraft:air"},
|
||||
{pos: [4, 4, 1], state: "minecraft:air"},
|
||||
{pos: [4, 4, 2], state: "minecraft:air"},
|
||||
{pos: [4, 4, 3], state: "minecraft:air"},
|
||||
{pos: [4, 4, 4], state: "minecraft:air"}
|
||||
],
|
||||
entities: [],
|
||||
palette: [
|
||||
"minecraft:polished_andesite",
|
||||
"minecraft:air",
|
||||
"computercraft:turtle_normal{facing:north,waterlogged:false}"
|
||||
]
|
||||
}
|
@@ -7,7 +7,6 @@ package dan200.computercraft.api.peripheral;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -35,7 +34,7 @@ public interface IPeripheral {
|
||||
* @see PeripheralType#getAdditionalTypes()
|
||||
*/
|
||||
default Set<String> getAdditionalTypes() {
|
||||
return Collections.emptySet();
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -6,7 +6,6 @@ package dan200.computercraft.api.peripheral;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -16,7 +15,7 @@ import java.util.Set;
|
||||
* lexicographically smallest non-empty name being chosen.
|
||||
*/
|
||||
public final class PeripheralType {
|
||||
private static final PeripheralType UNTYPED = new PeripheralType(null, Collections.emptySet());
|
||||
private static final PeripheralType UNTYPED = new PeripheralType(null, Set.of());
|
||||
|
||||
private final @Nullable String type;
|
||||
private final Set<String> additionalTypes;
|
||||
@@ -46,7 +45,7 @@ public final class PeripheralType {
|
||||
*/
|
||||
public static PeripheralType ofType(String type) {
|
||||
checkTypeName("type cannot be null or empty");
|
||||
return new PeripheralType(type, Collections.emptySet());
|
||||
return new PeripheralType(type, Set.of());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,8 +117,8 @@ public final class PeripheralType {
|
||||
}
|
||||
|
||||
private static Set<String> getTypes(Collection<String> types) {
|
||||
if (types.isEmpty()) return Collections.emptySet();
|
||||
if (types.size() == 1) return Collections.singleton(types.iterator().next());
|
||||
if (types.isEmpty()) return Set.of();
|
||||
if (types.size() == 1) return Set.of(types.iterator().next());
|
||||
return Set.copyOf(types);
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,8 @@
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
import cc.tweaked.gradle.getAbsolutePath
|
||||
|
||||
plugins {
|
||||
`java-library`
|
||||
`java-test-fixtures`
|
||||
@@ -24,13 +26,13 @@ dependencies {
|
||||
implementation(libs.netty.socks)
|
||||
implementation(libs.netty.proxy)
|
||||
implementation(libs.slf4j)
|
||||
implementation(libs.asm)
|
||||
|
||||
testFixturesImplementation(libs.slf4j)
|
||||
testFixturesApi(platform(libs.kotlin.platform))
|
||||
testFixturesApi(libs.bundles.test)
|
||||
testFixturesApi(libs.bundles.kotlin)
|
||||
|
||||
testImplementation(libs.asm)
|
||||
testImplementation(libs.bundles.test)
|
||||
testRuntimeOnly(libs.bundles.testRuntime)
|
||||
testRuntimeOnly(libs.slf4j.simple)
|
||||
@@ -45,7 +47,7 @@ tasks.processResources {
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
systemProperty("cct.test-files", buildDir.resolve("tmp/testFiles").absolutePath)
|
||||
systemProperty("cct.test-files", layout.buildDirectory.dir("tmp/testFiles").getAbsolutePath())
|
||||
}
|
||||
|
||||
tasks.testFixturesJar {
|
||||
|
@@ -8,8 +8,9 @@ import dan200.computercraft.api.lua.ILuaAPIFactory;
|
||||
import dan200.computercraft.core.asm.GenericMethod;
|
||||
import dan200.computercraft.core.asm.LuaMethodSupplier;
|
||||
import dan200.computercraft.core.asm.PeripheralMethodSupplier;
|
||||
import dan200.computercraft.core.computer.ComputerThread;
|
||||
import dan200.computercraft.core.computer.GlobalEnvironment;
|
||||
import dan200.computercraft.core.computer.computerthread.ComputerScheduler;
|
||||
import dan200.computercraft.core.computer.computerthread.ComputerThread;
|
||||
import dan200.computercraft.core.computer.mainthread.MainThreadScheduler;
|
||||
import dan200.computercraft.core.computer.mainthread.NoWorkMainThreadScheduler;
|
||||
import dan200.computercraft.core.lua.CobaltLuaMachine;
|
||||
@@ -31,7 +32,7 @@ import java.util.concurrent.TimeUnit;
|
||||
*/
|
||||
public final class ComputerContext {
|
||||
private final GlobalEnvironment globalEnvironment;
|
||||
private final ComputerThread computerScheduler;
|
||||
private final ComputerScheduler computerScheduler;
|
||||
private final MainThreadScheduler mainThreadScheduler;
|
||||
private final ILuaMachine.Factory luaFactory;
|
||||
private final List<ILuaAPIFactory> apiFactories;
|
||||
@@ -39,7 +40,7 @@ public final class ComputerContext {
|
||||
private final MethodSupplier<PeripheralMethod> peripheralMethods;
|
||||
|
||||
ComputerContext(
|
||||
GlobalEnvironment globalEnvironment, ComputerThread computerScheduler,
|
||||
GlobalEnvironment globalEnvironment, ComputerScheduler computerScheduler,
|
||||
MainThreadScheduler mainThreadScheduler, ILuaMachine.Factory luaFactory,
|
||||
List<ILuaAPIFactory> apiFactories, MethodSupplier<LuaMethod> luaMethods,
|
||||
MethodSupplier<PeripheralMethod> peripheralMethods
|
||||
@@ -68,7 +69,7 @@ public final class ComputerContext {
|
||||
*
|
||||
* @return The current computer thread manager.
|
||||
*/
|
||||
public ComputerThread computerScheduler() {
|
||||
public ComputerScheduler computerScheduler() {
|
||||
return computerScheduler;
|
||||
}
|
||||
|
||||
@@ -162,7 +163,7 @@ public final class ComputerContext {
|
||||
*/
|
||||
public static class Builder {
|
||||
private final GlobalEnvironment environment;
|
||||
private int threads = 1;
|
||||
private @Nullable ComputerScheduler computerScheduler = null;
|
||||
private @Nullable MainThreadScheduler mainThreadScheduler;
|
||||
private @Nullable ILuaMachine.Factory luaFactory;
|
||||
private @Nullable List<ILuaAPIFactory> apiFactories;
|
||||
@@ -173,7 +174,7 @@ public final class ComputerContext {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the number of threads the {@link ComputerThread} will use.
|
||||
* Set the {@link #computerScheduler()} to use {@link ComputerThread} with a given number of threads.
|
||||
*
|
||||
* @param threads The number of threads to use.
|
||||
* @return {@code this}, for chaining
|
||||
@@ -181,7 +182,20 @@ public final class ComputerContext {
|
||||
*/
|
||||
public Builder computerThreads(int threads) {
|
||||
if (threads < 1) throw new IllegalArgumentException("Threads must be >= 1");
|
||||
this.threads = threads;
|
||||
return computerScheduler(new ComputerThread(threads));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link ComputerScheduler} for this context.
|
||||
*
|
||||
* @param scheduler The computer thread scheduler.
|
||||
* @return {@code this}, for chaining
|
||||
* @see ComputerContext#mainThreadScheduler()
|
||||
*/
|
||||
public Builder computerScheduler(ComputerScheduler scheduler) {
|
||||
Objects.requireNonNull(scheduler);
|
||||
if (computerScheduler != null) throw new IllegalStateException("Computer scheduler already specified");
|
||||
computerScheduler = scheduler;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -250,7 +264,7 @@ public final class ComputerContext {
|
||||
public ComputerContext build() {
|
||||
return new ComputerContext(
|
||||
environment,
|
||||
new ComputerThread(threads),
|
||||
computerScheduler == null ? new ComputerThread(1) : computerScheduler,
|
||||
mainThreadScheduler == null ? new NoWorkMainThreadScheduler() : mainThreadScheduler,
|
||||
luaFactory == null ? CobaltLuaMachine::new : luaFactory,
|
||||
apiFactories == null ? List.of() : apiFactories,
|
||||
|
@@ -22,7 +22,6 @@ public final class CoreConfig {
|
||||
}
|
||||
|
||||
public static int maximumFilesOpen = 128;
|
||||
public static boolean disableLua51Features = false;
|
||||
public static String defaultComputerSettings = "";
|
||||
|
||||
public static boolean httpEnabled = true;
|
||||
|
@@ -18,7 +18,6 @@ import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaders;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -83,7 +82,7 @@ public class HTTPAPI implements ILuaAPI {
|
||||
var options = args.getTable(0);
|
||||
address = getStringField(options, "url");
|
||||
postString = optStringField(options, "body", null);
|
||||
headerTable = optTableField(options, "headers", Collections.emptyMap());
|
||||
headerTable = optTableField(options, "headers", Map.of());
|
||||
binary = optBooleanField(options, "binary", false);
|
||||
requestMethod = optStringField(options, "method", null);
|
||||
redirect = optBooleanField(options, "redirect", true);
|
||||
@@ -92,7 +91,7 @@ public class HTTPAPI implements ILuaAPI {
|
||||
// Get URL and post information
|
||||
address = args.getString(0);
|
||||
postString = args.optString(1, null);
|
||||
headerTable = args.optTable(2, Collections.emptyMap());
|
||||
headerTable = args.optTable(2, Map.of());
|
||||
binary = args.optBoolean(3, false);
|
||||
requestMethod = null;
|
||||
redirect = true;
|
||||
@@ -154,11 +153,11 @@ public class HTTPAPI implements ILuaAPI {
|
||||
if (args.get(0) instanceof Map) {
|
||||
var options = args.getTable(0);
|
||||
address = getStringField(options, "url");
|
||||
headerTable = optTableField(options, "headers", Collections.emptyMap());
|
||||
headerTable = optTableField(options, "headers", Map.of());
|
||||
timeoutArg = optRealField(options, "timeout");
|
||||
} else {
|
||||
address = args.getString(0);
|
||||
headerTable = args.optTable(1, Collections.emptyMap());
|
||||
headerTable = args.optTable(1, Map.of());
|
||||
timeoutArg = Optional.empty();
|
||||
}
|
||||
|
||||
|
@@ -12,7 +12,7 @@ import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.HandleGeneric;
|
||||
import dan200.computercraft.core.methods.ObjectSource;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -74,6 +74,6 @@ public class HttpResponseHandle implements ObjectSource {
|
||||
|
||||
@Override
|
||||
public Iterable<Object> getExtra() {
|
||||
return Collections.singletonList(reader);
|
||||
return List.of(reader);
|
||||
}
|
||||
}
|
||||
|
@@ -4,12 +4,12 @@
|
||||
|
||||
package dan200.computercraft.core.apis.http.websocket;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import dan200.computercraft.api.lua.*;
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
import dan200.computercraft.core.apis.http.options.Options;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import static dan200.computercraft.api.lua.LuaValues.checkFinite;
|
||||
@@ -106,12 +106,12 @@ public class WebsocketHandle {
|
||||
|
||||
@Override
|
||||
public MethodResult resume(Object[] event) {
|
||||
if (event.length >= 3 && Objects.equal(event[0], MESSAGE_EVENT) && Objects.equal(event[1], address)) {
|
||||
if (event.length >= 3 && Objects.equals(event[0], MESSAGE_EVENT) && Objects.equals(event[1], address)) {
|
||||
return MethodResult.of(Arrays.copyOfRange(event, 2, event.length));
|
||||
} else if (event.length >= 2 && Objects.equal(event[0], CLOSE_EVENT) && Objects.equal(event[1], address) && websocket.isClosed()) {
|
||||
} else if (event.length >= 2 && Objects.equals(event[0], CLOSE_EVENT) && Objects.equals(event[1], address) && websocket.isClosed()) {
|
||||
// If the socket is closed abort.
|
||||
return MethodResult.of();
|
||||
} else if (event.length >= 2 && timeoutId != -1 && Objects.equal(event[0], TIMER_EVENT)
|
||||
} else if (event.length >= 2 && timeoutId != -1 && Objects.equals(event[0], TIMER_EVENT)
|
||||
&& event[1] instanceof Number id && id.intValue() == timeoutId) {
|
||||
// If we received a matching timer event then abort.
|
||||
return MethodResult.of();
|
||||
|
@@ -9,7 +9,7 @@ import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
|
||||
import dan200.computercraft.core.methods.ObjectSource;
|
||||
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
@@ -42,6 +42,6 @@ public class TransferredFile implements ObjectSource {
|
||||
|
||||
@Override
|
||||
public Iterable<Object> getExtra() {
|
||||
return Collections.singleton(handle);
|
||||
return List.of(handle);
|
||||
}
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ package dan200.computercraft.core.apis.transfer;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@@ -18,15 +19,19 @@ public class TransferredFiles {
|
||||
public static final String EVENT = "file_transfer";
|
||||
|
||||
private final AtomicBoolean consumed = new AtomicBoolean(false);
|
||||
private final Runnable onConsumed;
|
||||
private final @Nullable Runnable onConsumed;
|
||||
|
||||
private final List<TransferredFile> files;
|
||||
|
||||
public TransferredFiles(List<TransferredFile> files, Runnable onConsumed) {
|
||||
public TransferredFiles(List<TransferredFile> files, @Nullable Runnable onConsumed) {
|
||||
this.files = files;
|
||||
this.onConsumed = onConsumed;
|
||||
}
|
||||
|
||||
public TransferredFiles(List<TransferredFile> files) {
|
||||
this(files, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* All the files that are being transferred to this computer.
|
||||
*
|
||||
@@ -40,6 +45,6 @@ public class TransferredFiles {
|
||||
|
||||
private void consumed() {
|
||||
if (consumed.getAndSet(true)) return;
|
||||
onConsumed.run();
|
||||
if (onConsumed != null) onConsumed.run();
|
||||
}
|
||||
}
|
||||
|
@@ -11,37 +11,31 @@ import com.google.common.primitives.Primitives;
|
||||
import com.google.common.reflect.TypeToken;
|
||||
import dan200.computercraft.api.lua.*;
|
||||
import dan200.computercraft.core.methods.LuaMethod;
|
||||
import org.objectweb.asm.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.lang.constant.ConstantDescs;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.reflect.Member;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.objectweb.asm.Opcodes.*;
|
||||
|
||||
/**
|
||||
* The underlying generator for {@link LuaFunction}-annotated methods.
|
||||
* <p>
|
||||
* The constructor {@link Generator#Generator(Class, List, Function)} takes in the type of interface to generate (i.e.
|
||||
* {@link LuaMethod}), the context arguments for this function (in the case of {@link LuaMethod}, this will just be
|
||||
* {@link ILuaContext}) and a "wrapper" function to lift a function to execute on the main thread.
|
||||
* The constructor {@link Generator#Generator(List, Function, Function)} takes in the type of interface to generate
|
||||
* (i.e. {@link LuaMethod}), the context arguments for this function (in the case of {@link LuaMethod}, this will just
|
||||
* be {@link ILuaContext}), a factory function (which invokes a method handle), and a "wrapper" function to lift a
|
||||
* function to execute on the main thread.
|
||||
* <p>
|
||||
* The generated class then implements this interface - the {@code apply} method calls the appropriate methods on
|
||||
* {@link IArguments} to extract the arguments, and then calls the original method.
|
||||
* <p>
|
||||
* As the method is not guaranteed to come from the same classloader, we cannot call the method directly, as that may
|
||||
* result in linkage errors. We instead inject a {@link MethodHandle} into the class as a dynamic constant, and then
|
||||
* call the method with {@link MethodHandle#invokeExact(Object...)}. The method handle is constant, and so this has
|
||||
* equivalent performance to the direct call.
|
||||
* For each input function, the generator then fabricates a {@link MethodHandle} which performs the argument validation,
|
||||
* and then calls the factory function to convert it to the desired interface.
|
||||
*
|
||||
* @param <T> The type of the interface the generated classes implement.
|
||||
*/
|
||||
@@ -49,47 +43,87 @@ final class Generator<T> {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Generator.class);
|
||||
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
|
||||
|
||||
private static final String METHOD_NAME = "apply";
|
||||
private static final String[] EXCEPTIONS = new String[]{ Type.getInternalName(LuaException.class) };
|
||||
private static final MethodHandle METHOD_RESULT_OF_VOID, METHOD_RESULT_OF_ONE, METHOD_RESULT_OF_MANY;
|
||||
|
||||
private static final String INTERNAL_METHOD_RESULT = Type.getInternalName(MethodResult.class);
|
||||
private static final String DESC_METHOD_RESULT = Type.getDescriptor(MethodResult.class);
|
||||
private static final Map<Class<?>, ArgMethods> argMethods;
|
||||
private static final ArgMethods ARG_TABLE_UNSAFE;
|
||||
private static final MethodHandle ARG_GET_OBJECT, ARG_GET_ENUM, ARG_OPT_ENUM, ARG_GET_STRING_COERCED;
|
||||
|
||||
private static final String INTERNAL_ARGUMENTS = Type.getInternalName(IArguments.class);
|
||||
private static final String DESC_ARGUMENTS = Type.getDescriptor(IArguments.class);
|
||||
private record ArgMethods(MethodHandle get, MethodHandle opt) {
|
||||
public static ArgMethods of(Class<?> type, String name) throws ReflectiveOperationException {
|
||||
return new ArgMethods(
|
||||
LOOKUP.findVirtual(IArguments.class, "get" + name, MethodType.methodType(type, int.class)),
|
||||
LOOKUP.findVirtual(IArguments.class, "opt" + name, MethodType.methodType(Optional.class, int.class))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static final String INTERNAL_COERCED = Type.getInternalName(Coerced.class);
|
||||
static void addArgType(Map<Class<?>, ArgMethods> types, Class<?> type, String name) throws ReflectiveOperationException {
|
||||
types.put(type, ArgMethods.of(type, name));
|
||||
}
|
||||
|
||||
private static final ConstantDynamic METHOD_CONSTANT = new ConstantDynamic(ConstantDescs.DEFAULT_NAME, MethodHandle.class.descriptorString(), new Handle(
|
||||
H_INVOKESTATIC, Type.getInternalName(MethodHandles.class), "classData",
|
||||
MethodType.methodType(Object.class, MethodHandles.Lookup.class, String.class, Class.class).descriptorString(), false
|
||||
));
|
||||
static {
|
||||
try {
|
||||
METHOD_RESULT_OF_VOID = LOOKUP.findStatic(MethodResult.class, "of", MethodType.methodType(MethodResult.class));
|
||||
METHOD_RESULT_OF_ONE = LOOKUP.findStatic(MethodResult.class, "of", MethodType.methodType(MethodResult.class, Object.class));
|
||||
METHOD_RESULT_OF_MANY = LOOKUP.findStatic(MethodResult.class, "of", MethodType.methodType(MethodResult.class, Object[].class));
|
||||
|
||||
Map<Class<?>, ArgMethods> argMethodMap = new HashMap<>();
|
||||
addArgType(argMethodMap, int.class, "Int");
|
||||
addArgType(argMethodMap, boolean.class, "Boolean");
|
||||
addArgType(argMethodMap, double.class, "Double");
|
||||
addArgType(argMethodMap, long.class, "Long");
|
||||
addArgType(argMethodMap, Map.class, "Table");
|
||||
addArgType(argMethodMap, String.class, "String");
|
||||
addArgType(argMethodMap, ByteBuffer.class, "Bytes");
|
||||
argMethods = Map.copyOf(argMethodMap);
|
||||
|
||||
ARG_TABLE_UNSAFE = ArgMethods.of(LuaTable.class, "TableUnsafe");
|
||||
ARG_GET_OBJECT = LOOKUP.findVirtual(IArguments.class, "get", MethodType.methodType(Object.class, int.class));
|
||||
ARG_GET_ENUM = LOOKUP.findVirtual(IArguments.class, "getEnum", MethodType.methodType(Enum.class, int.class, Class.class));
|
||||
ARG_OPT_ENUM = LOOKUP.findVirtual(IArguments.class, "optEnum", MethodType.methodType(Optional.class, int.class, Class.class));
|
||||
|
||||
// Create a new Coerced<>(args.getStringCoerced(_)) function.
|
||||
ARG_GET_STRING_COERCED = MethodHandles.filterReturnValue(
|
||||
setReturn(LOOKUP.findVirtual(IArguments.class, "getStringCoerced", MethodType.methodType(String.class, int.class)), Object.class),
|
||||
LOOKUP.findConstructor(Coerced.class, MethodType.methodType(void.class, Object.class))
|
||||
);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private final Class<T> base;
|
||||
private final List<Class<?>> context;
|
||||
private final List<Class<?>> contextWithArguments;
|
||||
private final MethodHandle argumentGetter;
|
||||
private final List<MethodHandle> contextGetters;
|
||||
|
||||
private final String[] interfaces;
|
||||
private final String methodDesc;
|
||||
private final String classPrefix;
|
||||
|
||||
private final Function<MethodHandle, T> factory;
|
||||
private final Function<T, T> wrap;
|
||||
|
||||
private final LoadingCache<Method, Optional<T>> methodCache = CacheBuilder
|
||||
.newBuilder()
|
||||
.build(CacheLoader.from(catching(this::build, Optional.empty())));
|
||||
|
||||
Generator(Class<T> base, List<Class<?>> context, Function<T, T> wrap) {
|
||||
this.base = base;
|
||||
Generator(List<Class<?>> context, Function<MethodHandle, T> factory, Function<T, T> wrap) {
|
||||
this.context = context;
|
||||
interfaces = new String[]{ Type.getInternalName(base) };
|
||||
this.factory = factory;
|
||||
this.wrap = wrap;
|
||||
|
||||
var methodDesc = new StringBuilder().append("(Ljava/lang/Object;");
|
||||
for (var klass : context) methodDesc.append(Type.getDescriptor(klass));
|
||||
methodDesc.append(DESC_ARGUMENTS).append(")").append(DESC_METHOD_RESULT);
|
||||
this.methodDesc = methodDesc.toString();
|
||||
var contextWithArguments = this.contextWithArguments = new ArrayList<>(context.size() + 1);
|
||||
contextWithArguments.addAll(context);
|
||||
contextWithArguments.add(IArguments.class);
|
||||
|
||||
classPrefix = Generator.class.getPackageName() + "." + base.getSimpleName() + "$";
|
||||
// Prepare a series of getters of the type (context..., IArguments) -> _ (or some prefix of this), for
|
||||
// extracting a single context value.
|
||||
argumentGetter = MethodHandles.dropArguments(MethodHandles.identity(IArguments.class), 0, context);
|
||||
|
||||
var contextGetters = this.contextGetters = new ArrayList<>(context.size());
|
||||
for (var i = 0; i < context.size(); i++) {
|
||||
var getter = MethodHandles.identity(context.get(i));
|
||||
if (i > 0) getter = MethodHandles.dropArguments(getter, 0, contextWithArguments.subList(0, i));
|
||||
contextGetters.add(getter);
|
||||
}
|
||||
}
|
||||
|
||||
Optional<T> getMethod(Method method) {
|
||||
@@ -131,184 +165,144 @@ final class Generator<T> {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// We have some rather ugly handling of static methods in both here and the main generate function. Static methods
|
||||
// only come from generic sources, so this should be safe.
|
||||
var target = Modifier.isStatic(modifiers) ? method.getParameterTypes()[0] : method.getDeclaringClass();
|
||||
|
||||
try {
|
||||
var handle = LOOKUP.unreflect(method);
|
||||
var originalHandle = LOOKUP.unreflect(method);
|
||||
|
||||
// Convert the handle from one of the form (target, ...) -> ret type to (Object, ...) -> Object. This both
|
||||
// handles the boxing of primitives for us, and ensures our bytecode does not reference any external types.
|
||||
// We could handle the conversion to MethodResult here too, but it doesn't feel worth it.
|
||||
var widenedHandle = handle.asType(widenMethodType(handle.type(), target));
|
||||
List<Type> parameters;
|
||||
if (Modifier.isStatic(modifiers)) {
|
||||
var allParameters = method.getGenericParameterTypes();
|
||||
parameters = Arrays.asList(allParameters).subList(1, allParameters.length);
|
||||
} else {
|
||||
parameters = Arrays.asList(method.getGenericParameterTypes());
|
||||
}
|
||||
|
||||
var bytes = generate(classPrefix + method.getName(), target, method, widenedHandle.type().descriptorString(), annotation.unsafe());
|
||||
if (bytes == null) return Optional.empty();
|
||||
var handle = buildMethodHandle(method, originalHandle, parameters, annotation.unsafe());
|
||||
if (handle == null) return Optional.empty();
|
||||
|
||||
var klass = LOOKUP.defineHiddenClassWithClassData(bytes, widenedHandle, true).lookupClass();
|
||||
|
||||
var instance = klass.asSubclass(base).getDeclaredConstructor().newInstance();
|
||||
var instance = factory.apply(handle);
|
||||
return Optional.of(annotation.mainThread() ? wrap.apply(instance) : instance);
|
||||
} catch (ReflectiveOperationException | ClassFormatError | RuntimeException e) {
|
||||
} catch (ReflectiveOperationException | RuntimeException e) {
|
||||
LOG.error("Error generating wrapper for {}.", name, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static MethodType widenMethodType(MethodType source, Class<?> target) {
|
||||
// Treat the target argument as just Object - we'll do the cast in the method handle.
|
||||
var args = source.parameterArray();
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
if (args[i] == target) args[i] = Object.class;
|
||||
}
|
||||
|
||||
// And convert the return value to Object if needed.
|
||||
var ret = source.returnType();
|
||||
return ret == void.class || ret == MethodResult.class || ret == Object[].class
|
||||
? MethodType.methodType(ret, args)
|
||||
: MethodType.methodType(Object.class, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given handle from type {@code (target, args...) -> ret} to {@code (Object, context..., IArguments) -> MethodResult},
|
||||
* inserting calls to {@link IArguments}'s getters, and wrapping the result with {@link MethodResult#of()}.
|
||||
*
|
||||
* @param method The original method, for error reporting.
|
||||
* @param handle The method handle to wrap.
|
||||
* @param parameterTypes The generic parameter types to this method. This should have the same type as the {@code handle}.
|
||||
* @param unsafe Whether to allow unsafe argument getters.
|
||||
* @return The wrapped method handle.
|
||||
*/
|
||||
@Nullable
|
||||
private byte[] generate(String className, Class<?> target, Method targetMethod, String targetDescriptor, boolean unsafe) {
|
||||
var internalName = className.replace(".", "/");
|
||||
|
||||
// Construct a public final class which extends Object and implements MethodInstance.Delegate
|
||||
var cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
|
||||
cw.visit(V17, ACC_PUBLIC | ACC_FINAL, internalName, null, "java/lang/Object", interfaces);
|
||||
cw.visitSource("CC generated method", null);
|
||||
|
||||
{ // Constructor just invokes super.
|
||||
var mw = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
|
||||
mw.visitCode();
|
||||
mw.visitVarInsn(ALOAD, 0);
|
||||
mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
|
||||
mw.visitInsn(RETURN);
|
||||
mw.visitMaxs(0, 0);
|
||||
mw.visitEnd();
|
||||
private MethodHandle buildMethodHandle(Member method, MethodHandle handle, List<Type> parameterTypes, boolean unsafe) {
|
||||
if (handle.type().parameterCount() != parameterTypes.size() + 1) {
|
||||
throw new IllegalArgumentException("Argument lists are mismatched");
|
||||
}
|
||||
|
||||
{
|
||||
var mw = cw.visitMethod(ACC_PUBLIC, METHOD_NAME, methodDesc, null, EXCEPTIONS);
|
||||
mw.visitCode();
|
||||
// We start off with a method handle of type (target, args...) -> _. We then append the context and IArguments
|
||||
// to the end, leaving a handle with type (target, args..., context..., IArguments) -> _.
|
||||
handle = MethodHandles.dropArguments(handle, handle.type().parameterCount(), contextWithArguments);
|
||||
|
||||
mw.visitLdcInsn(METHOD_CONSTANT);
|
||||
// Then for each argument, generate a method handle of type (context..., IArguments) -> _, which is used to
|
||||
// extract this argument.
|
||||
var argCount = 0;
|
||||
List<MethodHandle> argSelectors = new ArrayList<>(parameterTypes.size());
|
||||
for (var paramType : parameterTypes) {
|
||||
var paramClass = Reflect.getRawType(method, paramType, true);
|
||||
if (paramClass == null) return null;
|
||||
|
||||
// If we're an instance method, load the target as the first argument.
|
||||
if (!Modifier.isStatic(targetMethod.getModifiers())) mw.visitVarInsn(ALOAD, 1);
|
||||
|
||||
var argIndex = 0;
|
||||
for (var genericArg : targetMethod.getGenericParameterTypes()) {
|
||||
var loadedArg = loadArg(mw, target, targetMethod, unsafe, genericArg, argIndex);
|
||||
if (loadedArg == null) return null;
|
||||
if (loadedArg) argIndex++;
|
||||
}
|
||||
|
||||
mw.visitMethodInsn(INVOKEVIRTUAL, "java/lang/invoke/MethodHandle", "invokeExact", targetDescriptor, false);
|
||||
|
||||
// We allow a reasonable amount of flexibility on the return value's type. Alongside the obvious MethodResult,
|
||||
// we convert basic types into an immediate result.
|
||||
var ret = targetMethod.getReturnType();
|
||||
if (ret != MethodResult.class) {
|
||||
if (ret == void.class) {
|
||||
mw.visitMethodInsn(INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "()" + DESC_METHOD_RESULT, false);
|
||||
} else if (ret == Object[].class) {
|
||||
mw.visitMethodInsn(INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "([Ljava/lang/Object;)" + DESC_METHOD_RESULT, false);
|
||||
// We first generate a method handle of type (context..., IArguments) -> _, which is used to extract this
|
||||
// argument.
|
||||
MethodHandle argSelector;
|
||||
if (paramClass == IArguments.class) {
|
||||
argSelector = argumentGetter;
|
||||
} else {
|
||||
var idx = context.indexOf(paramClass);
|
||||
if (idx >= 0) {
|
||||
argSelector = contextGetters.get(idx);
|
||||
} else {
|
||||
mw.visitMethodInsn(INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "(Ljava/lang/Object;)" + DESC_METHOD_RESULT, false);
|
||||
var selector = loadArg(method, unsafe, paramClass, paramType, argCount++);
|
||||
if (selector == null) return null;
|
||||
argSelector = MethodHandles.filterReturnValue(argumentGetter, selector);
|
||||
}
|
||||
}
|
||||
|
||||
mw.visitInsn(ARETURN);
|
||||
|
||||
mw.visitMaxs(0, 0);
|
||||
mw.visitEnd();
|
||||
argSelectors.add(argSelector);
|
||||
}
|
||||
|
||||
cw.visitEnd();
|
||||
// Fold over the original method's arguments, excluding the target in reverse. For each argument, we reduce
|
||||
// a method of type type (target, args..., arg_n, context..., IArguments) -> _ to (target, args..., context..., IArguments) -> _
|
||||
// until eventually we've flattened the whole list.
|
||||
for (var i = parameterTypes.size() - 1; i >= 0; i--) {
|
||||
handle = MethodHandles.foldArguments(handle, i + 1, argSelectors.get(i));
|
||||
}
|
||||
|
||||
return cw.toByteArray();
|
||||
// Then cast the target to Object, so it's compatible with the desired type.
|
||||
handle = handle.asType(handle.type().changeParameterType(0, Object.class));
|
||||
|
||||
// Finally wrap the returned value into a MethodResult.
|
||||
var type = handle.type();
|
||||
var ret = type.returnType();
|
||||
if (ret == MethodResult.class) {
|
||||
return handle;
|
||||
} else if (ret == void.class) {
|
||||
return MethodHandles.filterReturnValue(handle, METHOD_RESULT_OF_VOID);
|
||||
} else if (ret == Object[].class) {
|
||||
return MethodHandles.filterReturnValue(handle, METHOD_RESULT_OF_MANY);
|
||||
} else {
|
||||
return MethodHandles.filterReturnValue(handle.asType(type.changeReturnType(Object.class)), METHOD_RESULT_OF_ONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Boolean loadArg(MethodVisitor mw, Class<?> target, Method method, boolean unsafe, java.lang.reflect.Type genericArg, int argIndex) {
|
||||
if (genericArg == target) {
|
||||
mw.visitVarInsn(ALOAD, 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
var arg = Reflect.getRawType(method, genericArg, true);
|
||||
if (arg == null) return null;
|
||||
|
||||
if (arg == IArguments.class) {
|
||||
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
var idx = context.indexOf(arg);
|
||||
if (idx >= 0) {
|
||||
mw.visitVarInsn(ALOAD, 2 + idx);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (arg == Coerced.class) {
|
||||
private static MethodHandle loadArg(Member method, boolean unsafe, Class<?> argType, Type genericArg, int argIndex) {
|
||||
if (argType == Coerced.class) {
|
||||
var klass = Reflect.getRawType(method, TypeToken.of(genericArg).resolveType(Reflect.COERCED_IN).getType(), false);
|
||||
if (klass == null) return null;
|
||||
|
||||
if (klass == String.class) {
|
||||
mw.visitTypeInsn(NEW, INTERNAL_COERCED);
|
||||
mw.visitInsn(DUP);
|
||||
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||
Reflect.loadInt(mw, argIndex);
|
||||
mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "getStringCoerced", "(I)Ljava/lang/String;", true);
|
||||
mw.visitMethodInsn(INVOKESPECIAL, INTERNAL_COERCED, "<init>", "(Ljava/lang/Object;)V", false);
|
||||
return true;
|
||||
}
|
||||
if (klass == String.class) return MethodHandles.insertArguments(ARG_GET_STRING_COERCED, 1, argIndex);
|
||||
}
|
||||
|
||||
if (arg == Optional.class) {
|
||||
var klass = Reflect.getRawType(method, TypeToken.of(genericArg).resolveType(Reflect.OPTIONAL_IN).getType(), false);
|
||||
if (klass == null) return null;
|
||||
if (argType == Optional.class) {
|
||||
var optType = Reflect.getRawType(method, TypeToken.of(genericArg).resolveType(Reflect.OPTIONAL_IN).getType(), false);
|
||||
if (optType == null) return null;
|
||||
|
||||
if (Enum.class.isAssignableFrom(klass) && klass != Enum.class) {
|
||||
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||
Reflect.loadInt(mw, argIndex);
|
||||
mw.visitLdcInsn(Type.getType(klass));
|
||||
mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "optEnum", "(ILjava/lang/Class;)Ljava/util/Optional;", true);
|
||||
return true;
|
||||
if (Enum.class.isAssignableFrom(optType) && optType != Enum.class) {
|
||||
return MethodHandles.insertArguments(ARG_OPT_ENUM, 1, argIndex, optType);
|
||||
}
|
||||
|
||||
var name = Reflect.getLuaName(Primitives.unwrap(klass), unsafe);
|
||||
if (name != null) {
|
||||
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||
Reflect.loadInt(mw, argIndex);
|
||||
mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "opt" + name, "(I)Ljava/util/Optional;", true);
|
||||
return true;
|
||||
}
|
||||
var getter = getArgMethods(Primitives.unwrap(optType), unsafe);
|
||||
if (getter != null) return MethodHandles.insertArguments(getter.opt(), 1, argIndex);
|
||||
}
|
||||
|
||||
if (Enum.class.isAssignableFrom(arg) && arg != Enum.class) {
|
||||
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||
Reflect.loadInt(mw, argIndex);
|
||||
mw.visitLdcInsn(Type.getType(arg));
|
||||
mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "getEnum", "(ILjava/lang/Class;)Ljava/lang/Enum;", true);
|
||||
mw.visitTypeInsn(CHECKCAST, Type.getInternalName(arg));
|
||||
return true;
|
||||
if (Enum.class.isAssignableFrom(argType) && argType != Enum.class) {
|
||||
return setReturn(MethodHandles.insertArguments(ARG_GET_ENUM, 1, argIndex, argType), argType);
|
||||
}
|
||||
|
||||
var name = arg == Object.class ? "" : Reflect.getLuaName(arg, unsafe);
|
||||
if (name != null) {
|
||||
if (Reflect.getRawType(method, genericArg, false) == null) return null;
|
||||
if (argType == Object.class) return MethodHandles.insertArguments(ARG_GET_OBJECT, 1, argIndex);
|
||||
|
||||
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||
Reflect.loadInt(mw, argIndex);
|
||||
mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "get" + name, "(I)" + Type.getDescriptor(arg), true);
|
||||
return true;
|
||||
}
|
||||
// Check we don't have a non-wildcard generic.
|
||||
if (Reflect.getRawType(method, genericArg, false) == null) return null;
|
||||
|
||||
LOG.error("Unknown parameter type {} for method {}.{}.",
|
||||
arg.getName(), method.getDeclaringClass().getName(), method.getName());
|
||||
var getter = getArgMethods(argType, unsafe);
|
||||
if (getter != null) return MethodHandles.insertArguments(getter.get(), 1, argIndex);
|
||||
|
||||
LOG.error("Unknown parameter type {} for method {}.{}.", argType.getName(), method.getDeclaringClass().getName(), method.getName());
|
||||
return null;
|
||||
}
|
||||
|
||||
private static MethodHandle setReturn(MethodHandle handle, Class<?> retTy) {
|
||||
return handle.asType(handle.type().changeReturnType(retTy));
|
||||
}
|
||||
|
||||
private static @Nullable ArgMethods getArgMethods(Class<?> type, boolean unsafe) {
|
||||
var getter = argMethods.get(type);
|
||||
if (getter != null) return getter;
|
||||
if (type == LuaTable.class && unsafe) return ARG_TABLE_UNSAFE;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@@ -6,6 +6,7 @@ package dan200.computercraft.core.asm;
|
||||
|
||||
import dan200.computercraft.api.lua.IDynamicLuaObject;
|
||||
import dan200.computercraft.api.lua.ILuaContext;
|
||||
import dan200.computercraft.api.lua.MethodResult;
|
||||
import dan200.computercraft.core.ComputerContext;
|
||||
import dan200.computercraft.core.methods.LuaMethod;
|
||||
import dan200.computercraft.core.methods.MethodSupplier;
|
||||
@@ -20,7 +21,14 @@ import java.util.Objects;
|
||||
* method supplier}. It should not be used directly.
|
||||
*/
|
||||
public final class LuaMethodSupplier {
|
||||
private static final Generator<LuaMethod> GENERATOR = new Generator<>(LuaMethod.class, List.of(ILuaContext.class),
|
||||
private static final Generator<LuaMethod> GENERATOR = new Generator<>(List.of(ILuaContext.class),
|
||||
m -> (target, context, args) -> {
|
||||
try {
|
||||
return (MethodResult) m.invokeExact(target, context, args);
|
||||
} catch (Throwable t) {
|
||||
throw ResultHelpers.throwUnchecked(t);
|
||||
}
|
||||
},
|
||||
m -> (target, context, args) -> {
|
||||
var escArgs = args.escapes();
|
||||
return context.executeMainThreadTask(() -> ResultHelpers.checkNormalResult(m.apply(target, context, escArgs)));
|
||||
|
@@ -4,9 +4,6 @@
|
||||
|
||||
package dan200.computercraft.core.asm;
|
||||
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.api.lua.MethodResult;
|
||||
import dan200.computercraft.api.peripheral.PeripheralType;
|
||||
@@ -23,7 +20,6 @@ import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static dan200.computercraft.core.asm.Generator.catching;
|
||||
@@ -36,9 +32,14 @@ final class MethodSupplierImpl<T> implements MethodSupplier<T> {
|
||||
private final IntCache<T> dynamic;
|
||||
private final Function<Object, String[]> dynamicMethods;
|
||||
|
||||
private final LoadingCache<Class<?>, List<NamedMethod<T>>> classCache = CacheBuilder
|
||||
.newBuilder()
|
||||
.build(CacheLoader.from(catching(this::getMethodsImpl, List.of())));
|
||||
private final ClassValue<List<NamedMethod<T>>> classCache = new ClassValue<>() {
|
||||
private final Function<Class<?>, List<NamedMethod<T>>> getter = catching(MethodSupplierImpl.this::getMethodsImpl, List.of());
|
||||
|
||||
@Override
|
||||
protected List<NamedMethod<T>> computeValue(Class<?> type) {
|
||||
return getter.apply(type);
|
||||
}
|
||||
};
|
||||
|
||||
MethodSupplierImpl(
|
||||
List<GenericMethod> genericMethods,
|
||||
@@ -93,12 +94,7 @@ final class MethodSupplierImpl<T> implements MethodSupplier<T> {
|
||||
|
||||
@VisibleForTesting
|
||||
List<NamedMethod<T>> getMethods(Class<?> klass) {
|
||||
try {
|
||||
return classCache.get(klass);
|
||||
} catch (ExecutionException e) {
|
||||
LOG.error("Error getting methods for {}.", klass.getName(), e.getCause());
|
||||
return List.of();
|
||||
}
|
||||
return classCache.get(klass);
|
||||
}
|
||||
|
||||
private List<NamedMethod<T>> getMethodsImpl(Class<?> klass) {
|
||||
@@ -148,5 +144,4 @@ final class MethodSupplierImpl<T> implements MethodSupplier<T> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@
|
||||
package dan200.computercraft.core.asm;
|
||||
|
||||
import dan200.computercraft.api.lua.ILuaContext;
|
||||
import dan200.computercraft.api.lua.MethodResult;
|
||||
import dan200.computercraft.api.peripheral.IComputerAccess;
|
||||
import dan200.computercraft.api.peripheral.IDynamicPeripheral;
|
||||
import dan200.computercraft.core.ComputerContext;
|
||||
@@ -21,7 +22,14 @@ import java.util.Objects;
|
||||
* method supplier}. It should not be used directly.
|
||||
*/
|
||||
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<>(List.of(ILuaContext.class, IComputerAccess.class),
|
||||
m -> (target, context, computer, args) -> {
|
||||
try {
|
||||
return (MethodResult) m.invokeExact(target, context, computer, args);
|
||||
} catch (Throwable t) {
|
||||
throw ResultHelpers.throwUnchecked(t);
|
||||
}
|
||||
},
|
||||
m -> (target, context, computer, args) -> {
|
||||
var escArgs = args.escapes();
|
||||
return context.executeMainThreadTask(() -> ResultHelpers.checkNormalResult(m.apply(target, context, computer, escArgs)));
|
||||
|
@@ -5,19 +5,13 @@
|
||||
package dan200.computercraft.core.asm;
|
||||
|
||||
import dan200.computercraft.api.lua.Coerced;
|
||||
import dan200.computercraft.api.lua.LuaTable;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.lang.reflect.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.objectweb.asm.Opcodes.ICONST_0;
|
||||
|
||||
final class Reflect {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Reflect.class);
|
||||
static final java.lang.reflect.Type OPTIONAL_IN = Optional.class.getTypeParameters()[0];
|
||||
@@ -27,24 +21,7 @@ final class Reflect {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static String getLuaName(Class<?> klass, boolean unsafe) {
|
||||
if (klass.isPrimitive()) {
|
||||
if (klass == int.class) return "Int";
|
||||
if (klass == boolean.class) return "Boolean";
|
||||
if (klass == double.class) return "Double";
|
||||
if (klass == long.class) return "Long";
|
||||
} else {
|
||||
if (klass == Map.class) return "Table";
|
||||
if (klass == String.class) return "String";
|
||||
if (klass == ByteBuffer.class) return "Bytes";
|
||||
if (klass == LuaTable.class && unsafe) return "TableUnsafe";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static Class<?> getRawType(Method method, Type root, boolean allowParameter) {
|
||||
static Class<?> getRawType(Member method, Type root, boolean allowParameter) {
|
||||
var underlying = root;
|
||||
while (true) {
|
||||
if (underlying instanceof Class<?> klass) return klass;
|
||||
@@ -71,12 +48,4 @@ final class Reflect {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static void loadInt(MethodVisitor visitor, int value) {
|
||||
if (value >= -1 && value <= 5) {
|
||||
visitor.visitInsn(ICONST_0 + value);
|
||||
} else {
|
||||
visitor.visitLdcInsn(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -22,4 +22,13 @@ final class ResultHelpers {
|
||||
|
||||
return result.getResult();
|
||||
}
|
||||
|
||||
static RuntimeException throwUnchecked(Throwable t) {
|
||||
return throwUnchecked0(t);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T extends Throwable> T throwUnchecked0(Throwable t) throws T {
|
||||
throw (T) t;
|
||||
}
|
||||
}
|
||||
|
@@ -4,7 +4,6 @@
|
||||
|
||||
package dan200.computercraft.core.computer;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import dan200.computercraft.api.lua.ILuaAPI;
|
||||
import dan200.computercraft.api.lua.ILuaContext;
|
||||
import dan200.computercraft.api.lua.LuaTask;
|
||||
@@ -16,6 +15,7 @@ import dan200.computercraft.core.filesystem.FileSystem;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@@ -140,7 +140,7 @@ public class Computer {
|
||||
}
|
||||
|
||||
public void setLabel(@Nullable String label) {
|
||||
if (!Objects.equal(label, this.label)) {
|
||||
if (!Objects.equals(label, this.label)) {
|
||||
this.label = label;
|
||||
externalOutputChanged.set(true);
|
||||
}
|
||||
|
@@ -10,6 +10,8 @@ import dan200.computercraft.api.lua.ILuaAPI;
|
||||
import dan200.computercraft.core.ComputerContext;
|
||||
import dan200.computercraft.core.CoreConfig;
|
||||
import dan200.computercraft.core.apis.*;
|
||||
import dan200.computercraft.core.computer.computerthread.ComputerScheduler;
|
||||
import dan200.computercraft.core.computer.computerthread.ComputerThread;
|
||||
import dan200.computercraft.core.filesystem.FileSystem;
|
||||
import dan200.computercraft.core.filesystem.FileSystemException;
|
||||
import dan200.computercraft.core.lua.ILuaMachine;
|
||||
@@ -17,7 +19,6 @@ import dan200.computercraft.core.lua.MachineEnvironment;
|
||||
import dan200.computercraft.core.lua.MachineException;
|
||||
import dan200.computercraft.core.methods.LuaMethod;
|
||||
import dan200.computercraft.core.methods.MethodSupplier;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
import dan200.computercraft.core.util.Nullability;
|
||||
@@ -25,13 +26,13 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
@@ -54,7 +55,7 @@ import java.util.concurrent.locks.ReentrantLock;
|
||||
* One final responsibility for the executor is calling {@link ILuaAPI#update()} every tick, via the {@link #tick()}
|
||||
* method. This should only be called when the computer is actually on ({@link #isOn}).
|
||||
*/
|
||||
final class ComputerExecutor {
|
||||
final class ComputerExecutor implements ComputerScheduler.Worker {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ComputerExecutor.class);
|
||||
private static final int QUEUE_LIMIT = 256;
|
||||
|
||||
@@ -62,9 +63,7 @@ final class ComputerExecutor {
|
||||
private final ComputerEnvironment computerEnvironment;
|
||||
private final MetricsObserver metrics;
|
||||
private final List<ApiWrapper> apis = new ArrayList<>();
|
||||
private final ComputerThread scheduler;
|
||||
private final MethodSupplier<LuaMethod> luaMethods;
|
||||
final TimeoutState timeout;
|
||||
|
||||
private @Nullable FileSystem fileSystem;
|
||||
|
||||
@@ -92,34 +91,11 @@ final class ComputerExecutor {
|
||||
private final ReentrantLock isOnLock = new ReentrantLock();
|
||||
|
||||
/**
|
||||
* A lock used for any changes to {@link #eventQueue}, {@link #command} or {@link #onComputerQueue}. This will be
|
||||
* used on the main thread, so locks should be kept as brief as possible.
|
||||
* A lock used for any changes to {@link #eventQueue} or {@link #command}. This will be used on the main thread,
|
||||
* so locks should be kept as brief as possible.
|
||||
*/
|
||||
private final Object queueLock = new Object();
|
||||
|
||||
/**
|
||||
* Determines if this executor is present within {@link ComputerThread}.
|
||||
*
|
||||
* @see #queueLock
|
||||
* @see #enqueue()
|
||||
* @see #afterWork()
|
||||
*/
|
||||
volatile boolean onComputerQueue = false;
|
||||
|
||||
/**
|
||||
* The amount of time this computer has used on a theoretical machine which shares work evenly amongst computers.
|
||||
*
|
||||
* @see ComputerThread
|
||||
*/
|
||||
long virtualRuntime = 0;
|
||||
|
||||
/**
|
||||
* The last time at which we updated {@link #virtualRuntime}.
|
||||
*
|
||||
* @see ComputerThread
|
||||
*/
|
||||
long vRuntimeStart;
|
||||
|
||||
/**
|
||||
* The command that {@link #work()} should execute on the computer thread.
|
||||
* <p>
|
||||
@@ -129,6 +105,7 @@ final class ComputerExecutor {
|
||||
* Note, if command is not {@code null}, then some command is scheduled to be executed. Otherwise it is not
|
||||
* currently in the queue (or is currently being executed).
|
||||
*/
|
||||
@GuardedBy("queueLock")
|
||||
private volatile @Nullable StateCommand command;
|
||||
|
||||
/**
|
||||
@@ -136,43 +113,45 @@ final class ComputerExecutor {
|
||||
* <p>
|
||||
* Note, this should be empty if this computer is off - it is cleared on shutdown and when turning on again.
|
||||
*/
|
||||
@GuardedBy("queueLock")
|
||||
private final Queue<Event> eventQueue = new ArrayDeque<>(4);
|
||||
|
||||
/**
|
||||
* Whether we interrupted an event and so should resume it instead of executing another task.
|
||||
* Whether this computer was paused (and so should resume without pulling an event) or not.
|
||||
*
|
||||
* @see #timeRemaining
|
||||
* @see #work()
|
||||
* @see #resumeMachine(String, Object[])
|
||||
*/
|
||||
private boolean interruptedEvent = false;
|
||||
private boolean wasPaused;
|
||||
|
||||
/**
|
||||
* The amount of time this computer can run for before being interrupted. This is only defined when
|
||||
* {@link #wasPaused} is set.
|
||||
*/
|
||||
private long timeRemaining = 0;
|
||||
|
||||
/**
|
||||
* Whether this executor has been closed, and will no longer accept any incoming commands or events.
|
||||
*
|
||||
* @see #queueStop(boolean, boolean)
|
||||
*/
|
||||
@GuardedBy("queueLock")
|
||||
private boolean closed;
|
||||
|
||||
private @Nullable WritableMount rootMount;
|
||||
|
||||
/**
|
||||
* The thread the executor is running on. This is non-null when performing work. We use this to ensure we're only
|
||||
* doing one bit of work at one time.
|
||||
*
|
||||
* @see ComputerThread
|
||||
*/
|
||||
final AtomicReference<Thread> executingThread = new AtomicReference<>();
|
||||
|
||||
private final ILuaMachine.Factory luaFactory;
|
||||
|
||||
private final ComputerScheduler.Executor executor;
|
||||
|
||||
ComputerExecutor(Computer computer, ComputerEnvironment computerEnvironment, ComputerContext context) {
|
||||
this.computer = computer;
|
||||
this.computerEnvironment = computerEnvironment;
|
||||
metrics = computerEnvironment.getMetrics();
|
||||
luaFactory = context.luaFactory();
|
||||
scheduler = context.computerScheduler();
|
||||
luaMethods = context.luaMethods();
|
||||
timeout = new TimeoutState(scheduler);
|
||||
executor = context.computerScheduler().createExecutor(this, metrics);
|
||||
|
||||
var environment = computer.getEnvironment();
|
||||
|
||||
@@ -192,6 +171,11 @@ final class ComputerExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getComputerID() {
|
||||
return computer.getID();
|
||||
}
|
||||
|
||||
boolean isOn() {
|
||||
return isOn;
|
||||
}
|
||||
@@ -202,10 +186,6 @@ final class ComputerExecutor {
|
||||
return fileSystem;
|
||||
}
|
||||
|
||||
Computer getComputer() {
|
||||
return computer;
|
||||
}
|
||||
|
||||
void addApi(ILuaAPI api) {
|
||||
apis.add(new ApiWrapper(api, null));
|
||||
}
|
||||
@@ -219,8 +199,8 @@ final class ComputerExecutor {
|
||||
if (closed || isOn || command != null) return;
|
||||
|
||||
command = StateCommand.TURN_ON;
|
||||
enqueue();
|
||||
}
|
||||
enqueue();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,24 +225,31 @@ final class ComputerExecutor {
|
||||
}
|
||||
|
||||
command = newCommand;
|
||||
enqueue();
|
||||
}
|
||||
enqueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort this whole computer due to a timeout. This will immediately destroy the Lua machine,
|
||||
* and then schedule a shutdown.
|
||||
*/
|
||||
void abort() {
|
||||
immediateFail(StateCommand.ABORT);
|
||||
@Override
|
||||
public void abortWithTimeout() {
|
||||
immediateFail(StateCommand.ABORT_WITH_TIMEOUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort this whole computer due to an internal error. This will immediately destroy the Lua machine,
|
||||
* and then schedule a shutdown.
|
||||
*/
|
||||
void fastFail() {
|
||||
immediateFail(StateCommand.ERROR);
|
||||
@Override
|
||||
public void abortWithError() {
|
||||
immediateFail(StateCommand.ABORT_WITH_ERROR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unload() {
|
||||
queueStop(false, true);
|
||||
}
|
||||
|
||||
private void immediateFail(StateCommand command) {
|
||||
@@ -293,17 +280,15 @@ final class ComputerExecutor {
|
||||
if (closed || command != null || eventQueue.size() >= QUEUE_LIMIT) return;
|
||||
|
||||
eventQueue.offer(new Event(event, args));
|
||||
enqueue();
|
||||
}
|
||||
enqueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add this executor to the {@link ComputerThread} if not already there.
|
||||
*/
|
||||
private void enqueue() {
|
||||
synchronized (queueLock) {
|
||||
if (!onComputerQueue) scheduler.queue(this);
|
||||
}
|
||||
executor.submit();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -384,7 +369,7 @@ final class ComputerExecutor {
|
||||
// Create the lua machine
|
||||
try (var bios = biosStream) {
|
||||
return luaFactory.create(new MachineEnvironment(
|
||||
new LuaContext(computer), metrics, timeout,
|
||||
new LuaContext(computer), metrics, executor.timeoutState(),
|
||||
() -> apis.stream().map(ApiWrapper::api).iterator(),
|
||||
luaMethods,
|
||||
computer.getGlobalEnvironment().getHostString()
|
||||
@@ -404,7 +389,6 @@ final class ComputerExecutor {
|
||||
try {
|
||||
// Reset the terminal and event queue
|
||||
computer.getTerminal().reset();
|
||||
interruptedEvent = false;
|
||||
synchronized (queueLock) {
|
||||
eventQueue.clear();
|
||||
}
|
||||
@@ -432,15 +416,15 @@ final class ComputerExecutor {
|
||||
isOnLock.unlock();
|
||||
}
|
||||
|
||||
// Now actually start the computer, now that everything is set up.
|
||||
resumeMachine(null, null);
|
||||
// Mark the Lua VM as ready to be executed next time.
|
||||
wasPaused = true;
|
||||
timeRemaining = TimeoutState.TIMEOUT;
|
||||
}
|
||||
|
||||
private void shutdown() throws InterruptedException {
|
||||
isOnLock.lockInterruptibly();
|
||||
try {
|
||||
isOn = false;
|
||||
interruptedEvent = false;
|
||||
isOn = wasPaused = false;
|
||||
synchronized (queueLock) {
|
||||
eventQueue.clear();
|
||||
}
|
||||
@@ -468,36 +452,6 @@ final class ComputerExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before calling {@link #work()}, setting up any important state.
|
||||
*/
|
||||
void beforeWork() {
|
||||
vRuntimeStart = System.nanoTime();
|
||||
timeout.startTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after executing {@link #work()}.
|
||||
*
|
||||
* @return If we have more work to do.
|
||||
*/
|
||||
boolean afterWork() {
|
||||
if (interruptedEvent) {
|
||||
timeout.pauseTimer();
|
||||
} else {
|
||||
timeout.stopTimer();
|
||||
}
|
||||
|
||||
metrics.observe(Metrics.COMPUTER_TASKS, timeout.nanoCurrent());
|
||||
|
||||
if (interruptedEvent) return true;
|
||||
|
||||
synchronized (queueLock) {
|
||||
if (eventQueue.isEmpty() && command == null) return onComputerQueue = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The main worker function, called by {@link ComputerThread}.
|
||||
* <p>
|
||||
@@ -507,15 +461,15 @@ final class ComputerExecutor {
|
||||
* @see #command
|
||||
* @see #eventQueue
|
||||
*/
|
||||
void work() throws InterruptedException {
|
||||
if (interruptedEvent && !closed) {
|
||||
interruptedEvent = false;
|
||||
if (machine != null) {
|
||||
resumeMachine(null, null);
|
||||
return;
|
||||
}
|
||||
@Override
|
||||
public void work() throws InterruptedException {
|
||||
workImpl();
|
||||
synchronized (queueLock) {
|
||||
if (wasPaused || command != null || !eventQueue.isEmpty()) enqueue();
|
||||
}
|
||||
}
|
||||
|
||||
private void workImpl() throws InterruptedException {
|
||||
StateCommand command;
|
||||
Event event = null;
|
||||
synchronized (queueLock) {
|
||||
@@ -523,7 +477,7 @@ final class ComputerExecutor {
|
||||
this.command = null;
|
||||
|
||||
// If we've no command, pull something from the event queue instead.
|
||||
if (command == null) {
|
||||
if (command == null && !wasPaused) {
|
||||
if (!isOn) {
|
||||
// We're not on and had no command, but we had work queued. This should never happen, so clear
|
||||
// the event queue just in case.
|
||||
@@ -536,6 +490,7 @@ final class ComputerExecutor {
|
||||
}
|
||||
|
||||
if (command != null) {
|
||||
wasPaused = false;
|
||||
switch (command) {
|
||||
case TURN_ON -> {
|
||||
if (isOn) return;
|
||||
@@ -552,23 +507,29 @@ final class ComputerExecutor {
|
||||
shutdown();
|
||||
computer.turnOn();
|
||||
}
|
||||
case ABORT -> {
|
||||
case ABORT_WITH_TIMEOUT -> {
|
||||
if (!isOn) return;
|
||||
displayFailure("Error running computer", TimeoutState.ABORT_MESSAGE);
|
||||
shutdown();
|
||||
}
|
||||
case ERROR -> {
|
||||
case ABORT_WITH_ERROR -> {
|
||||
if (!isOn) return;
|
||||
displayFailure("Error running computer", "An internal error occurred, see logs.");
|
||||
shutdown();
|
||||
}
|
||||
}
|
||||
} else if (wasPaused) {
|
||||
executor.setRemainingTime(timeRemaining);
|
||||
resumeMachine(null, null);
|
||||
} else if (event != null) {
|
||||
executor.setRemainingTime(TimeoutState.TIMEOUT);
|
||||
resumeMachine(event.name, event.args);
|
||||
}
|
||||
}
|
||||
|
||||
void printState(StringBuilder out) {
|
||||
@Override
|
||||
@SuppressWarnings("GuardedBy")
|
||||
public void writeState(StringBuilder out) {
|
||||
out.append("Enqueued command: ").append(command).append('\n');
|
||||
out.append("Enqueued events: ").append(eventQueue.size()).append('\n');
|
||||
|
||||
@@ -599,19 +560,23 @@ final class ComputerExecutor {
|
||||
|
||||
private void resumeMachine(@Nullable String event, @Nullable Object[] args) throws InterruptedException {
|
||||
var result = Nullability.assertNonNull(machine).handleEvent(event, args);
|
||||
interruptedEvent = result.isPause();
|
||||
if (!result.isError()) return;
|
||||
|
||||
displayFailure("Error running computer", result.getMessage());
|
||||
shutdown();
|
||||
if (result.isError()) {
|
||||
displayFailure("Error running computer", result.getMessage());
|
||||
shutdown();
|
||||
} else if (result.isPause()) {
|
||||
wasPaused = true;
|
||||
timeRemaining = executor.getRemainingTime();
|
||||
} else {
|
||||
wasPaused = false;
|
||||
}
|
||||
}
|
||||
|
||||
private enum StateCommand {
|
||||
TURN_ON,
|
||||
SHUTDOWN,
|
||||
REBOOT,
|
||||
ABORT,
|
||||
ERROR,
|
||||
ABORT_WITH_TIMEOUT,
|
||||
ABORT_WITH_ERROR,
|
||||
}
|
||||
|
||||
private record Event(String name, @Nullable Object[] args) {
|
||||
|
@@ -12,7 +12,6 @@ import dan200.computercraft.core.apis.ComputerAccess;
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -43,7 +42,7 @@ public class ComputerSystem extends ComputerAccess implements IComputerSystem {
|
||||
@Override
|
||||
public Map<String, IPeripheral> getAvailablePeripherals() {
|
||||
// TODO: Should this return peripherals on the current computer?
|
||||
return Collections.emptyMap();
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
@@ -5,6 +5,8 @@
|
||||
package dan200.computercraft.core.computer;
|
||||
|
||||
import com.google.errorprone.annotations.concurrent.GuardedBy;
|
||||
import dan200.computercraft.core.computer.computerthread.ComputerScheduler;
|
||||
import dan200.computercraft.core.computer.computerthread.ManagedTimeoutState;
|
||||
import dan200.computercraft.core.lua.ILuaMachine;
|
||||
import dan200.computercraft.core.lua.MachineResult;
|
||||
|
||||
@@ -24,90 +26,54 @@ import java.util.concurrent.TimeUnit;
|
||||
* (namely, throwing a "Too long without yielding" error).
|
||||
* <p>
|
||||
* Now, if a computer still does not stop after that period, they're behaving really badly. 1.5 seconds after a soft
|
||||
* abort ({@link #ABORT_TIMEOUT}), we trigger a hard abort (note, this is done from the computer thread manager). This
|
||||
* will destroy the entire Lua runtime and shut the computer down.
|
||||
* abort ({@link #ABORT_TIMEOUT}), we trigger a hard abort. This will destroy the entire Lua runtime and shut the
|
||||
* computer down.
|
||||
* <p>
|
||||
* The Lua runtime is also allowed to pause execution if there are other computers contesting for work. All computers
|
||||
* are allowed to run for {@link ComputerThread#scaledPeriod()} nanoseconds (see {@link #currentDeadline}). After that
|
||||
* period, if any computers are waiting to be executed then we'll set the paused flag to true ({@link #isPaused()}.
|
||||
* are guaranteed to run for some time. After that period, if any computers are waiting to be executed then we'll set
|
||||
* the paused flag to true ({@link #isPaused()}.
|
||||
*
|
||||
* @see ComputerThread
|
||||
* @see ComputerScheduler
|
||||
* @see ManagedTimeoutState
|
||||
* @see ILuaMachine
|
||||
* @see MachineResult#isPause()
|
||||
*/
|
||||
public final class TimeoutState {
|
||||
public abstract class TimeoutState {
|
||||
/**
|
||||
* The total time a task is allowed to run before aborting in nanoseconds.
|
||||
* The time (in nanoseconds) are computer is allowed to run for its long-running tasks, such as startup and
|
||||
* shutdown.
|
||||
*/
|
||||
static final long TIMEOUT = TimeUnit.MILLISECONDS.toNanos(7000);
|
||||
public static final long BASE_TIMEOUT = TimeUnit.SECONDS.toNanos(30);
|
||||
|
||||
/**
|
||||
* The total time the Lua VM is allowed to run before aborting in nanoseconds.
|
||||
*/
|
||||
public static final long TIMEOUT = TimeUnit.MILLISECONDS.toNanos(7000);
|
||||
|
||||
/**
|
||||
* The time the task is allowed to run after each abort in nanoseconds.
|
||||
*/
|
||||
static final long ABORT_TIMEOUT = TimeUnit.MILLISECONDS.toNanos(1500);
|
||||
public static final long ABORT_TIMEOUT = TimeUnit.MILLISECONDS.toNanos(1500);
|
||||
|
||||
/**
|
||||
* The error message to display when we trigger an abort.
|
||||
*/
|
||||
public static final String ABORT_MESSAGE = "Too long without yielding";
|
||||
|
||||
private final ComputerThread scheduler;
|
||||
@GuardedBy("this")
|
||||
private final List<Runnable> listeners = new ArrayList<>(0);
|
||||
|
||||
private boolean paused;
|
||||
private boolean softAbort;
|
||||
private volatile boolean hardAbort;
|
||||
|
||||
/**
|
||||
* When the cumulative time would have started had the whole event been processed in one go.
|
||||
*/
|
||||
private long cumulativeStart;
|
||||
|
||||
/**
|
||||
* How much cumulative time has elapsed. This is effectively {@code cumulativeStart - currentStart}.
|
||||
*/
|
||||
private long cumulativeElapsed;
|
||||
|
||||
/**
|
||||
* When this execution round started.
|
||||
*/
|
||||
private long currentStart;
|
||||
|
||||
/**
|
||||
* When this execution round should look potentially be paused.
|
||||
*/
|
||||
private long currentDeadline;
|
||||
|
||||
public TimeoutState(ComputerThread scheduler) {
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
long nanoCumulative() {
|
||||
return System.nanoTime() - cumulativeStart;
|
||||
}
|
||||
|
||||
long nanoCurrent() {
|
||||
return System.nanoTime() - currentStart;
|
||||
}
|
||||
protected boolean paused;
|
||||
protected boolean softAbort;
|
||||
protected volatile boolean hardAbort;
|
||||
|
||||
/**
|
||||
* Recompute the {@link #isSoftAborted()} and {@link #isPaused()} flags.
|
||||
* <p>
|
||||
* Normally this will be called automatically by the {@link ComputerScheduler}, but it may be useful to call this
|
||||
* manually if the most up-to-date information is needed.
|
||||
*/
|
||||
public synchronized void refresh() {
|
||||
// Important: The weird arithmetic here is important, as nanoTime may return negative values, and so we
|
||||
// need to handle overflow.
|
||||
var now = System.nanoTime();
|
||||
var changed = false;
|
||||
if (!paused && (paused = currentDeadline - now <= 0 && scheduler.hasPendingWork())) { // now >= currentDeadline
|
||||
changed = true;
|
||||
}
|
||||
if (!softAbort && (softAbort = now - cumulativeStart - TIMEOUT >= 0)) { // now - cumulativeStart >= TIMEOUT
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) updateListeners();
|
||||
}
|
||||
public abstract void refresh();
|
||||
|
||||
/**
|
||||
* Whether we should pause execution of this machine.
|
||||
@@ -117,7 +83,7 @@ public final class TimeoutState {
|
||||
*
|
||||
* @return Whether we should pause execution.
|
||||
*/
|
||||
public boolean isPaused() {
|
||||
public final boolean isPaused() {
|
||||
return paused;
|
||||
}
|
||||
|
||||
@@ -126,7 +92,7 @@ public final class TimeoutState {
|
||||
*
|
||||
* @return {@code true} if we should throw a timeout error.
|
||||
*/
|
||||
public boolean isSoftAborted() {
|
||||
public final boolean isSoftAborted() {
|
||||
return softAbort;
|
||||
}
|
||||
|
||||
@@ -135,64 +101,22 @@ public final class TimeoutState {
|
||||
*
|
||||
* @return {@code true} if the machine should be forcibly shut down.
|
||||
*/
|
||||
public boolean isHardAborted() {
|
||||
public final boolean isHardAborted() {
|
||||
return hardAbort;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the machine should be forcibly aborted.
|
||||
*/
|
||||
void hardAbort() {
|
||||
softAbort = hardAbort = true;
|
||||
synchronized (this) {
|
||||
updateListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the current and cumulative timers again.
|
||||
*/
|
||||
void startTimer() {
|
||||
var now = System.nanoTime();
|
||||
currentStart = now;
|
||||
currentDeadline = now + scheduler.scaledPeriod();
|
||||
// Compute the "nominal start time".
|
||||
cumulativeStart = now - cumulativeElapsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the cumulative time, to be resumed by {@link #startTimer()}.
|
||||
*
|
||||
* @see #nanoCumulative()
|
||||
*/
|
||||
synchronized void pauseTimer() {
|
||||
// We set the cumulative time to difference between current time and "nominal start time".
|
||||
cumulativeElapsed = System.nanoTime() - cumulativeStart;
|
||||
paused = false;
|
||||
updateListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the cumulative time and resets the abort flags.
|
||||
*/
|
||||
synchronized void stopTimer() {
|
||||
cumulativeElapsed = 0;
|
||||
paused = softAbort = hardAbort = false;
|
||||
updateListeners();
|
||||
}
|
||||
|
||||
@GuardedBy("this")
|
||||
private void updateListeners() {
|
||||
protected final void updateListeners() {
|
||||
for (var listener : listeners) listener.run();
|
||||
}
|
||||
|
||||
public synchronized void addListener(Runnable listener) {
|
||||
public final synchronized void addListener(Runnable listener) {
|
||||
Objects.requireNonNull(listener, "listener cannot be null");
|
||||
listeners.add(listener);
|
||||
listener.run();
|
||||
}
|
||||
|
||||
public synchronized void removeListener(Runnable listener) {
|
||||
public final synchronized void removeListener(Runnable listener) {
|
||||
Objects.requireNonNull(listener, "listener cannot be null");
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
@@ -0,0 +1,127 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.computer.computerthread;
|
||||
|
||||
import dan200.computercraft.core.computer.TimeoutState;
|
||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* The {@link ComputerScheduler} is responsible for executing computers on the computer thread(s).
|
||||
* <p>
|
||||
* This handles both scheduling the computers for work across multiple threads, as well as {@linkplain TimeoutState timing out}
|
||||
* or pausing the computer if they execute for too long.
|
||||
* <p>
|
||||
* This API is composed of two interfaces, a {@link Worker} and {@link Executor}. The {@link ComputerScheduler}
|
||||
* implementation will supply an {@link Executor}, while consuming classes should implement {@link Worker}.
|
||||
* <p>
|
||||
* In practice, this interface is only implemented by {@link ComputerThread} (and consumed by {@link dan200.computercraft.core.computer.ComputerExecutor}),
|
||||
* however this interface is useful to enforce separation of the two.
|
||||
*
|
||||
* @see ManagedTimeoutState
|
||||
*/
|
||||
public interface ComputerScheduler {
|
||||
Executor createExecutor(Worker worker, MetricsObserver metrics);
|
||||
|
||||
boolean stop(long timeout, TimeUnit unit) throws InterruptedException;
|
||||
|
||||
/**
|
||||
* The {@link Executor} holds the state of a {@link Worker} within the scheduler.
|
||||
* <p>
|
||||
* This is used to schedule the worker for execution, as well as providing some additional control over the
|
||||
* {@link TimeoutState}.
|
||||
*/
|
||||
interface Executor {
|
||||
/**
|
||||
* Submit the executor to the scheduler, marking it as ready {@linkplain Worker#work() to run some work}.
|
||||
* <p>
|
||||
* This function is idempotent - if the executor is already queued, nothing will happen.
|
||||
*/
|
||||
void submit();
|
||||
|
||||
/**
|
||||
* Get the executor's {@link TimeoutState}.
|
||||
*
|
||||
* @return The executor's timeout state.
|
||||
*/
|
||||
TimeoutState timeoutState();
|
||||
|
||||
/**
|
||||
* Get the amount of time this computer can run for before being interrupted.
|
||||
* <p>
|
||||
* This value starts off as {@link TimeoutState#BASE_TIMEOUT}, but may be reduced by
|
||||
* {@link #setRemainingTime(long)}.
|
||||
*
|
||||
* @return The time this computer can run for being interrupted.
|
||||
* @see #getRemainingTime()
|
||||
*/
|
||||
long getRemainingTime();
|
||||
|
||||
/**
|
||||
* Set the amount of this computer can execute for before being interrupted.
|
||||
* <p>
|
||||
* This value will typically be {@link TimeoutState#TIMEOUT}, but may be a previous value of
|
||||
* {@link #getRemainingTime()} if the computer is resuming after {@linkplain TimeoutState#isPaused() being
|
||||
* paused}.
|
||||
*
|
||||
* @param time The time this computer can execute for.
|
||||
* @see #getRemainingTime()
|
||||
*/
|
||||
void setRemainingTime(long time);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Worker} is responsible for actually running the computer's code.
|
||||
* <p>
|
||||
* his handles {@linkplain Worker#work() running the actual computer logic}, as well as providing some additional
|
||||
* control methods.
|
||||
* <p>
|
||||
* This should be implemented by the consuming class.
|
||||
*/
|
||||
interface Worker {
|
||||
/**
|
||||
* Perform any work that the computer needs to do, for instance turning on, shutting down or actually running
|
||||
* code.
|
||||
* <p>
|
||||
* If the computer needs to run immediately again, it should call {@link Executor#submit()} within this method.
|
||||
*
|
||||
* @throws InterruptedException If the computer has run for too long and must be terminated.
|
||||
*/
|
||||
void work() throws InterruptedException;
|
||||
|
||||
/**
|
||||
* Get the ID of this computer, used in error messages.
|
||||
*
|
||||
* @return This computers ID.
|
||||
*/
|
||||
int getComputerID();
|
||||
|
||||
/**
|
||||
* Write any useful debugging information computer to the provided buffer. This is used in log messages when the
|
||||
* computer has run for too long.
|
||||
*
|
||||
* @param output The buffer to write to.
|
||||
*/
|
||||
void writeState(StringBuilder output);
|
||||
|
||||
/**
|
||||
* Abort this whole computer due to a timeout.
|
||||
*/
|
||||
void abortWithTimeout();
|
||||
|
||||
/**
|
||||
* Abort this whole computer due to some internal error.
|
||||
*/
|
||||
void abortWithError();
|
||||
|
||||
/**
|
||||
* "Unload" this computer, shutting it down and preventing it from running again.
|
||||
* <p>
|
||||
* This is called by the scheduler when {@linkplain ComputerScheduler#stop(long, TimeUnit) it is stopped.}
|
||||
*/
|
||||
void unload();
|
||||
}
|
||||
}
|
@@ -2,26 +2,26 @@
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.computer;
|
||||
package dan200.computercraft.core.computer.computerthread;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.errorprone.annotations.concurrent.GuardedBy;
|
||||
import dan200.computercraft.core.ComputerContext;
|
||||
import dan200.computercraft.core.Logging;
|
||||
import dan200.computercraft.core.computer.TimeoutState;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||
import dan200.computercraft.core.util.ThreadUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
@@ -29,9 +29,9 @@ import java.util.concurrent.locks.ReentrantLock;
|
||||
/**
|
||||
* Runs all scheduled tasks for computers in a {@link ComputerContext}.
|
||||
* <p>
|
||||
* This acts as an over-complicated {@link ThreadPoolExecutor}: It creates several {@link Worker} threads which pull
|
||||
* tasks from a shared queue, executing them. It also creates a single {@link Monitor} thread, which updates computer
|
||||
* timeouts, killing workers if they have not been terminated by {@link TimeoutState#isSoftAborted()}.
|
||||
* This acts as an over-complicated {@link ThreadPoolExecutor}: It creates several {@linkplain WorkerThread worker
|
||||
* threads} which pull tasks from a shared queue, executing them. It also creates a single {@link Monitor} thread, which
|
||||
* updates computer timeouts, killing workers if they have not been terminated by {@link TimeoutState#isSoftAborted()}.
|
||||
* <p>
|
||||
* Computers are executed using a priority system, with those who have spent less time executing having a higher
|
||||
* priority than those hogging the thread. This, combined with {@link TimeoutState#isPaused()} means we can reduce the
|
||||
@@ -39,20 +39,16 @@ import java.util.concurrent.locks.ReentrantLock;
|
||||
* <p>
|
||||
* This is done using an implementation of Linux's Completely Fair Scheduler. When a computer executes, we compute what
|
||||
* share of execution time it has used (time executed/number of tasks). We then pick the computer who has the least
|
||||
* "virtual execution time" (aka {@link ComputerExecutor#virtualRuntime}).
|
||||
* "virtual execution time" (aka {@link ExecutorImpl#virtualRuntime}).
|
||||
* <p>
|
||||
* When adding a computer to the queue, we make sure its "virtual runtime" is at least as big as the smallest runtime.
|
||||
* This means that adding computers which have slept a lot do not then have massive priority over everyone else. See
|
||||
* {@link #queue(ComputerExecutor)} for how this is implemented.
|
||||
* {@link #queue(ExecutorImpl)} for how this is implemented.
|
||||
* <p>
|
||||
* In reality, it's unlikely that more than a few computers are waiting to execute at once, so this will not have much
|
||||
* effect unless you have a computer hogging execution time. However, it is pretty effective in those situations.
|
||||
*
|
||||
* @see TimeoutState For how hard timeouts are handled.
|
||||
* @see ComputerExecutor For how computers actually do execution.
|
||||
*/
|
||||
@SuppressWarnings("GuardedBy") // FIXME: Hard to know what the correct thing to do is.
|
||||
public final class ComputerThread {
|
||||
public final class ComputerThread implements ComputerScheduler {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ComputerThread.class);
|
||||
|
||||
/**
|
||||
@@ -96,7 +92,7 @@ public final class ComputerThread {
|
||||
/**
|
||||
* Time difference between reporting crashed threads.
|
||||
*
|
||||
* @see Worker#reportTimeout(ComputerExecutor, long)
|
||||
* @see WorkerThread#reportTimeout(ExecutorImpl, long)
|
||||
*/
|
||||
private static final long REPORT_DEBOUNCE = TimeUnit.SECONDS.toNanos(1);
|
||||
|
||||
@@ -123,7 +119,7 @@ public final class ComputerThread {
|
||||
* The array of current workers, and their owning threads.
|
||||
*/
|
||||
@GuardedBy("threadLock")
|
||||
private final Worker[] workers;
|
||||
private final WorkerThread[] workers;
|
||||
|
||||
/**
|
||||
* The number of workers in {@link #workers}.
|
||||
@@ -137,29 +133,33 @@ public final class ComputerThread {
|
||||
private final long minPeriod;
|
||||
|
||||
private final ReentrantLock computerLock = new ReentrantLock();
|
||||
private final Condition workerWakeup = computerLock.newCondition();
|
||||
private final Condition monitorWakeup = computerLock.newCondition();
|
||||
private final @GuardedBy("computerLock") Condition workerWakeup = computerLock.newCondition();
|
||||
private final @GuardedBy("computerLock") Condition monitorWakeup = computerLock.newCondition();
|
||||
|
||||
private final AtomicInteger idleWorkers = new AtomicInteger(0);
|
||||
|
||||
/**
|
||||
* Active queues to execute.
|
||||
*/
|
||||
private final TreeSet<ComputerExecutor> computerQueue = new TreeSet<>((a, b) -> {
|
||||
@GuardedBy("computerLock")
|
||||
private final TreeSet<ExecutorImpl> computerQueue = new TreeSet<>(ComputerThread::compareExecutors);
|
||||
|
||||
@SuppressWarnings("GuardedBy")
|
||||
private static int compareExecutors(ExecutorImpl a, ExecutorImpl b) {
|
||||
if (a == b) return 0; // Should never happen, but let's be consistent here
|
||||
|
||||
long at = a.virtualRuntime, bt = b.virtualRuntime;
|
||||
if (at == bt) return Integer.compare(a.hashCode(), b.hashCode());
|
||||
return at < bt ? -1 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The minimum {@link ComputerExecutor#virtualRuntime} time on the tree.
|
||||
* The minimum {@link ExecutorImpl#virtualRuntime} time on the tree.
|
||||
*/
|
||||
private long minimumVirtualRuntime = 0;
|
||||
|
||||
public ComputerThread(int threadCount) {
|
||||
workers = new Worker[threadCount];
|
||||
workers = new WorkerThread[threadCount];
|
||||
|
||||
// latency and minPeriod are scaled by 1 + floor(log2(threads)). We can afford to execute tasks for
|
||||
// longer when executing on more than one thread.
|
||||
@@ -168,13 +168,28 @@ public final class ComputerThread {
|
||||
minPeriod = DEFAULT_MIN_PERIOD * factor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Executor createExecutor(ComputerScheduler.Worker worker, MetricsObserver metrics) {
|
||||
return new ExecutorImpl(worker, metrics);
|
||||
}
|
||||
|
||||
@GuardedBy("threadLock")
|
||||
private void addWorker(int index) {
|
||||
LOG.trace("Spawning new worker {}.", index);
|
||||
(workers[index] = new Worker(index)).owner.start();
|
||||
(workers[index] = new WorkerThread(index)).owner.start();
|
||||
workerCount++;
|
||||
}
|
||||
|
||||
@SuppressWarnings("GuardedBy")
|
||||
private int workerCount() {
|
||||
return workerCount;
|
||||
}
|
||||
|
||||
@SuppressWarnings("GuardedBy")
|
||||
private WorkerThread[] workersReadOnly() {
|
||||
return workers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure sufficient workers are running.
|
||||
*/
|
||||
@@ -182,7 +197,7 @@ public final class ComputerThread {
|
||||
private void ensureRunning() {
|
||||
// Don't even enter the lock if we've a monitor and don't need to/can't spawn an additional worker.
|
||||
// We'll be holding the computer lock at this point, so there's no problems with idleWorkers being wrong.
|
||||
if (monitor != null && (idleWorkers.get() > 0 || workerCount == workers.length)) return;
|
||||
if (monitor != null && (idleWorkers.get() > 0 || workerCount() == workersReadOnly().length)) return;
|
||||
|
||||
threadLock.lock();
|
||||
try {
|
||||
@@ -217,6 +232,7 @@ public final class ComputerThread {
|
||||
* @return Whether the thread was successfully shut down.
|
||||
* @throws InterruptedException If interrupted while waiting.
|
||||
*/
|
||||
@Override
|
||||
public boolean stop(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
advanceState(STOPPING);
|
||||
|
||||
@@ -269,12 +285,12 @@ public final class ComputerThread {
|
||||
/**
|
||||
* Mark a computer as having work, enqueuing it on the thread.
|
||||
* <p>
|
||||
* You must be holding {@link ComputerExecutor}'s {@code queueLock} when calling this method - it should only
|
||||
* You must be holding {@link ExecutorImpl}'s {@code queueLock} when calling this method - it should only
|
||||
* be called from {@code enqueue}.
|
||||
*
|
||||
* @param executor The computer to execute work on.
|
||||
*/
|
||||
void queue(ComputerExecutor executor) {
|
||||
void queue(ExecutorImpl executor) {
|
||||
computerLock.lock();
|
||||
try {
|
||||
if (state.get() != RUNNING) throw new IllegalStateException("ComputerThread is no longer running");
|
||||
@@ -282,9 +298,6 @@ public final class ComputerThread {
|
||||
// Ensure we've got a worker running.
|
||||
ensureRunning();
|
||||
|
||||
if (executor.onComputerQueue) throw new IllegalStateException("Cannot queue already queued executor");
|
||||
executor.onComputerQueue = true;
|
||||
|
||||
updateRuntimes(null);
|
||||
|
||||
// We're not currently on the queue, so update its current execution time to
|
||||
@@ -316,14 +329,15 @@ public final class ComputerThread {
|
||||
|
||||
|
||||
/**
|
||||
* Update the {@link ComputerExecutor#virtualRuntime}s of all running tasks, and then update the
|
||||
* Update the {@link ExecutorImpl#virtualRuntime}s of all running tasks, and then update the
|
||||
* {@link #minimumVirtualRuntime} based on the current tasks.
|
||||
* <p>
|
||||
* This is called before queueing tasks, to ensure that {@link #minimumVirtualRuntime} is up-to-date.
|
||||
*
|
||||
* @param current The machine which we updating runtimes from.
|
||||
*/
|
||||
private void updateRuntimes(@Nullable ComputerExecutor current) {
|
||||
@GuardedBy("computerLock")
|
||||
private void updateRuntimes(@Nullable ExecutorImpl current) {
|
||||
var minRuntime = Long.MAX_VALUE;
|
||||
|
||||
// If we've a task on the queue, use that as our base time.
|
||||
@@ -332,7 +346,7 @@ public final class ComputerThread {
|
||||
// Update all the currently executing tasks
|
||||
var now = System.nanoTime();
|
||||
var tasks = 1 + computerQueue.size();
|
||||
for (@Nullable var runner : workers) {
|
||||
for (@Nullable var runner : workersReadOnly()) {
|
||||
if (runner == null) continue;
|
||||
var executor = runner.currentExecutor.get();
|
||||
if (executor == null) continue;
|
||||
@@ -357,22 +371,9 @@ public final class ComputerThread {
|
||||
* Ensure the "currently working" state of the executor is reset, the timings are updated, and then requeue the
|
||||
* executor if needed.
|
||||
*
|
||||
* @param runner The runner this task was on.
|
||||
* @param executor The executor to requeue
|
||||
*/
|
||||
private void afterWork(Worker runner, ComputerExecutor executor) {
|
||||
// Clear the executor's thread.
|
||||
var currentThread = executor.executingThread.getAndSet(null);
|
||||
if (currentThread != runner.owner) {
|
||||
|
||||
LOG.error(
|
||||
"Expected computer #{} to be running on {}, but already running on {}. This is a SERIOUS bug, please report with your debug.log.",
|
||||
executor.getComputer().getID(),
|
||||
runner.owner.getName(),
|
||||
currentThread == null ? "nothing" : currentThread.getName()
|
||||
);
|
||||
}
|
||||
|
||||
private void afterWork(ExecutorImpl executor) {
|
||||
computerLock.lock();
|
||||
try {
|
||||
updateRuntimes(executor);
|
||||
@@ -388,6 +389,13 @@ public final class ComputerThread {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("GuardedBy")
|
||||
private int computerQueueSize() {
|
||||
// FIXME: We access this on other threads (in TimeoutState), so their reads won't be consistent. This isn't
|
||||
// "critical" behaviour, so not clear if it matters too much.
|
||||
return computerQueue.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* The scaled period for a single task.
|
||||
*
|
||||
@@ -397,11 +405,8 @@ public final class ComputerThread {
|
||||
* @see #LATENCY_MAX_TASKS
|
||||
*/
|
||||
long scaledPeriod() {
|
||||
// FIXME: We access this on other threads (in TimeoutState), so their reads won't be consistent. This isn't
|
||||
// "critical" behaviour, so not clear if it matters too much.
|
||||
|
||||
// +1 to include the current task
|
||||
var count = 1 + computerQueue.size();
|
||||
var count = 1 + computerQueueSize();
|
||||
return count < LATENCY_MAX_TASKS ? latency / count : minPeriod;
|
||||
}
|
||||
|
||||
@@ -411,9 +416,8 @@ public final class ComputerThread {
|
||||
* @return If we have work queued up.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public boolean hasPendingWork() {
|
||||
// FIXME: See comment in scaledPeriod. Again, we access this in multiple threads but not clear if it matters!
|
||||
return !computerQueue.isEmpty();
|
||||
boolean hasPendingWork() {
|
||||
return computerQueueSize() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -422,12 +426,11 @@ public final class ComputerThread {
|
||||
*
|
||||
* @return If the computer threads are busy.
|
||||
*/
|
||||
@GuardedBy("computerLock")
|
||||
private boolean isBusy() {
|
||||
return computerQueue.size() > idleWorkers.get();
|
||||
return computerQueueSize() > idleWorkers.get();
|
||||
}
|
||||
|
||||
private void workerFinished(Worker worker) {
|
||||
private void workerFinished(WorkerThread worker) {
|
||||
// We should only shut down a worker once! This should only happen if we fail to abort a worker and then the
|
||||
// worker finishes normally.
|
||||
if (!worker.running.getAndSet(false)) return;
|
||||
@@ -442,6 +445,7 @@ public final class ComputerThread {
|
||||
workerCount--;
|
||||
|
||||
if (workers[worker.index] != worker) {
|
||||
assert false : "workerFinished but inconsistent worker";
|
||||
LOG.error("Worker {} closed, but new runner has been spawned.", worker.index);
|
||||
} else if (state.get() == RUNNING || (state.get() == STOPPING && hasPendingWork())) {
|
||||
addWorker(worker.index);
|
||||
@@ -455,7 +459,7 @@ public final class ComputerThread {
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes all currently active {@link Worker}s and terminates their tasks once they have exceeded the hard
|
||||
* Observes all currently active {@link WorkerThread}s and terminates their tasks once they have exceeded the hard
|
||||
* abort limit.
|
||||
*
|
||||
* @see TimeoutState
|
||||
@@ -491,7 +495,7 @@ public final class ComputerThread {
|
||||
}
|
||||
|
||||
private void checkRunners() {
|
||||
for (@Nullable var runner : workers) {
|
||||
for (@Nullable var runner : workersReadOnly()) {
|
||||
if (runner == null) continue;
|
||||
|
||||
// If the worker has no work, skip
|
||||
@@ -503,25 +507,28 @@ public final class ComputerThread {
|
||||
|
||||
// If we're still within normal execution times (TIMEOUT) or soft abort (ABORT_TIMEOUT),
|
||||
// then we can let the Lua machine do its work.
|
||||
var afterStart = executor.timeout.nanoCumulative();
|
||||
var afterHardAbort = afterStart - TimeoutState.TIMEOUT - TimeoutState.ABORT_TIMEOUT;
|
||||
var remainingTime = executor.timeout.getRemainingTime();
|
||||
// If remainingTime > 0, then we're executing normally,
|
||||
// If remainingTime > -ABORT_TIMEOUT, then we've soft aborted.
|
||||
// Otherwise, remainingTime <= -ABORT_TIMEOUT, and we've run over by -ABORT_TIMEOUT - remainingTime.
|
||||
var afterHardAbort = -remainingTime - TimeoutState.ABORT_TIMEOUT;
|
||||
if (afterHardAbort < 0) continue;
|
||||
|
||||
// Set the hard abort flag.
|
||||
executor.timeout.hardAbort();
|
||||
executor.abort();
|
||||
executor.worker.abortWithTimeout();
|
||||
|
||||
if (afterHardAbort >= TimeoutState.ABORT_TIMEOUT * 2) {
|
||||
// If we've hard aborted and interrupted, and we're still not dead, then mark the worker
|
||||
// as dead, finish off the task, and spawn a new runner.
|
||||
runner.reportTimeout(executor, afterStart);
|
||||
runner.reportTimeout(executor, remainingTime);
|
||||
runner.owner.interrupt();
|
||||
|
||||
workerFinished(runner);
|
||||
} else if (afterHardAbort >= TimeoutState.ABORT_TIMEOUT) {
|
||||
// If we've hard aborted but we're still not dead, dump the stack trace and interrupt
|
||||
// the task.
|
||||
runner.reportTimeout(executor, afterStart);
|
||||
runner.reportTimeout(executor, remainingTime);
|
||||
runner.owner.interrupt();
|
||||
}
|
||||
}
|
||||
@@ -531,11 +538,11 @@ public final class ComputerThread {
|
||||
/**
|
||||
* Pulls tasks from the {@link #computerQueue} queue and runs them.
|
||||
* <p>
|
||||
* This is responsible for running the {@link ComputerExecutor#work()}, {@link ComputerExecutor#beforeWork()} and
|
||||
* {@link ComputerExecutor#afterWork()} functions. Everything else is either handled by the executor, timeout
|
||||
* state or monitor.
|
||||
* This is responsible for running the {@link ComputerScheduler.Worker#work()}, {@link ExecutorImpl#beforeWork()}
|
||||
* and {@link ExecutorImpl#afterWork()} functions. Everything else is either handled by the executor,
|
||||
* timeout state or monitor.
|
||||
*/
|
||||
private final class Worker implements Runnable {
|
||||
private final class WorkerThread implements Runnable {
|
||||
/**
|
||||
* The index into the {@link #workers} array.
|
||||
*/
|
||||
@@ -550,21 +557,21 @@ public final class ComputerThread {
|
||||
* Whether this runner is currently executing. This may be set to false when this worker terminates, or when
|
||||
* we try to abandon a worker in the monitor
|
||||
*
|
||||
* @see #workerFinished(Worker)
|
||||
* @see #workerFinished(WorkerThread)
|
||||
*/
|
||||
final AtomicBoolean running = new AtomicBoolean(true);
|
||||
|
||||
/**
|
||||
* The computer we're currently running.
|
||||
*/
|
||||
final AtomicReference<ComputerExecutor> currentExecutor = new AtomicReference<>(null);
|
||||
final AtomicReference<ExecutorImpl> currentExecutor = new AtomicReference<>(null);
|
||||
|
||||
/**
|
||||
* The last time we reported a stack trace, used to avoid spamming the logs.
|
||||
*/
|
||||
AtomicLong lastReport = new AtomicLong(Long.MIN_VALUE);
|
||||
|
||||
Worker(int index) {
|
||||
WorkerThread(int index) {
|
||||
this.index = index;
|
||||
owner = workerFactory.newThread(this);
|
||||
}
|
||||
@@ -579,10 +586,9 @@ public final class ComputerThread {
|
||||
}
|
||||
|
||||
private void runImpl() {
|
||||
tasks:
|
||||
while (running.get()) {
|
||||
// Wait for an active queue to execute
|
||||
ComputerExecutor executor;
|
||||
ExecutorImpl executor;
|
||||
computerLock.lock();
|
||||
try {
|
||||
idleWorkers.getAndIncrement();
|
||||
@@ -597,21 +603,18 @@ public final class ComputerThread {
|
||||
computerLock.unlock();
|
||||
}
|
||||
|
||||
// If we're trying to executing some task on this computer while someone else is doing work, something
|
||||
// is seriously wrong.
|
||||
while (!executor.executingThread.compareAndSet(null, owner)) {
|
||||
var existing = executor.executingThread.get();
|
||||
if (existing != null) {
|
||||
LOG.error(
|
||||
"Trying to run computer #{} on thread {}, but already running on {}. This is a SERIOUS bug, please report with your debug.log.",
|
||||
executor.getComputer().getID(), owner.getName(), existing.getName()
|
||||
);
|
||||
continue tasks;
|
||||
}
|
||||
// Mark this computer as executing.
|
||||
if (!ExecutorImpl.STATE.compareAndSet(executor, ExecutorState.ON_QUEUE, ExecutorState.RUNNING)) {
|
||||
assert false : "Running computer on the wrong thread";
|
||||
LOG.error(
|
||||
"Trying to run computer #{} on thread {}, but already running on another thread. This is a SERIOUS " +
|
||||
"bug, please report with your debug.log.",
|
||||
executor.worker.getComputerID(), owner.getName()
|
||||
);
|
||||
}
|
||||
|
||||
// If we're stopping, the only thing this executor should be doing is shutting down.
|
||||
if (state.get() >= STOPPING) executor.queueStop(false, true);
|
||||
if (state.get() >= STOPPING) executor.worker.unload();
|
||||
|
||||
// Reset the timers
|
||||
executor.beforeWork();
|
||||
@@ -622,19 +625,19 @@ public final class ComputerThread {
|
||||
|
||||
// Execute the task
|
||||
try {
|
||||
executor.work();
|
||||
executor.worker.work();
|
||||
} catch (Exception | LinkageError | VirtualMachineError e) {
|
||||
LOG.error("Error running task on computer #" + executor.getComputer().getID(), e);
|
||||
LOG.error("Error running task on computer #" + executor.worker.getComputerID(), e);
|
||||
// Tear down the computer immediately. There's no guarantee it's well-behaved from now on.
|
||||
executor.fastFail();
|
||||
executor.worker.abortWithError();
|
||||
} finally {
|
||||
var thisExecutor = currentExecutor.getAndSet(null);
|
||||
if (thisExecutor != null) afterWork(this, executor);
|
||||
if (thisExecutor != null) afterWork(executor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void reportTimeout(ComputerExecutor executor, long time) {
|
||||
private void reportTimeout(ExecutorImpl executor, long time) {
|
||||
if (!LOG.isErrorEnabled(Logging.COMPUTER_ERROR)) return;
|
||||
|
||||
// Attempt to debounce stack trace reporting, limiting ourselves to one every second. There's no need to be
|
||||
@@ -647,8 +650,8 @@ public final class ComputerThread {
|
||||
var owner = Objects.requireNonNull(this.owner);
|
||||
|
||||
var builder = new StringBuilder()
|
||||
.append("Terminating computer #").append(executor.getComputer().getID())
|
||||
.append(" due to timeout (running for ").append(time * 1e-9)
|
||||
.append("Terminating computer #").append(executor.worker.getComputerID())
|
||||
.append(" due to timeout (ran over by ").append(time * -1e-9)
|
||||
.append(" seconds). This is NOT a bug, but may mean a computer is misbehaving.\n")
|
||||
.append("Thread ")
|
||||
.append(owner.getName())
|
||||
@@ -662,9 +665,150 @@ public final class ComputerThread {
|
||||
builder.append(" at ").append(element).append('\n');
|
||||
}
|
||||
|
||||
executor.printState(builder);
|
||||
executor.worker.writeState(builder);
|
||||
|
||||
LOG.warn(builder.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The current state of a {@link ExecutorState}.
|
||||
* <p>
|
||||
* Executors are either enqueued (have more work to do) or not and working or not. This enum encapsulates the four
|
||||
* combinations of these properties, with the following transitions:
|
||||
*
|
||||
* <pre>{@code
|
||||
* submit() afterWork()
|
||||
* IDLE ---------> ON_QUEUE <----------- REPEAT
|
||||
* ^ | ^
|
||||
* | | runImpl() |
|
||||
* | V |
|
||||
* +---------------RUNNING----------------+
|
||||
* afterWork() submit()
|
||||
* }</pre>
|
||||
*/
|
||||
enum ExecutorState {
|
||||
/**
|
||||
* This executor is idle.
|
||||
*/
|
||||
IDLE,
|
||||
|
||||
/**
|
||||
* This executor is on the queue but idle.
|
||||
*/
|
||||
ON_QUEUE,
|
||||
|
||||
/**
|
||||
* This executor is running and will transition to idle after execution.
|
||||
*/
|
||||
RUNNING,
|
||||
|
||||
/**
|
||||
* This executor is running and should run again after this task finishes.
|
||||
*/
|
||||
REPEAT;
|
||||
|
||||
ExecutorState enqueue() {
|
||||
return switch (this) {
|
||||
case IDLE, ON_QUEUE -> ON_QUEUE;
|
||||
case RUNNING, REPEAT -> REPEAT;
|
||||
};
|
||||
}
|
||||
|
||||
ExecutorState requeue() {
|
||||
return switch (this) {
|
||||
case IDLE, ON_QUEUE -> {
|
||||
assert false : "Impossible state after executing";
|
||||
LOG.error("Impossible state - calling requeue with {}.", this);
|
||||
yield ExecutorState.ON_QUEUE;
|
||||
}
|
||||
case RUNNING -> ExecutorState.IDLE;
|
||||
case REPEAT -> ExecutorState.ON_QUEUE;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private final class ExecutorImpl implements Executor {
|
||||
public static final AtomicReferenceFieldUpdater<ExecutorImpl, ExecutorState> STATE = AtomicReferenceFieldUpdater.newUpdater(
|
||||
ExecutorImpl.class, ExecutorState.class, "$state"
|
||||
);
|
||||
|
||||
final Worker worker;
|
||||
private final MetricsObserver metrics;
|
||||
final TimeoutImpl timeout;
|
||||
|
||||
/**
|
||||
* The current state of this worker.
|
||||
*/
|
||||
private volatile ExecutorState $state = ExecutorState.IDLE;
|
||||
|
||||
/**
|
||||
* The amount of time this computer has used on a theoretical machine which shares work evenly amongst computers.
|
||||
*
|
||||
* @see ComputerThread
|
||||
*/
|
||||
long virtualRuntime = 0;
|
||||
|
||||
/**
|
||||
* The last time at which we updated {@link #virtualRuntime}.
|
||||
*
|
||||
* @see ComputerThread
|
||||
*/
|
||||
long vRuntimeStart;
|
||||
|
||||
ExecutorImpl(Worker worker, MetricsObserver metrics) {
|
||||
this.worker = worker;
|
||||
this.metrics = metrics;
|
||||
timeout = new TimeoutImpl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before calling {@link Worker#work()}, setting up any important state.
|
||||
*/
|
||||
void beforeWork() {
|
||||
vRuntimeStart = System.nanoTime();
|
||||
timeout.startTimer(scaledPeriod());
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after executing {@link Worker#work()}.
|
||||
*
|
||||
* @return If we have more work to do.
|
||||
*/
|
||||
boolean afterWork() {
|
||||
timeout.reset();
|
||||
metrics.observe(Metrics.COMPUTER_TASKS, timeout.getExecutionTime());
|
||||
|
||||
var state = STATE.getAndUpdate(this, ExecutorState::requeue);
|
||||
return state == ExecutorState.REPEAT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void submit() {
|
||||
var state = STATE.getAndUpdate(this, ExecutorState::enqueue);
|
||||
if (state == ExecutorState.IDLE) queue(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeoutState timeoutState() {
|
||||
return timeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getRemainingTime() {
|
||||
return timeout.getRemainingTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRemainingTime(long time) {
|
||||
timeout.setRemainingTime(time);
|
||||
}
|
||||
}
|
||||
|
||||
private final class TimeoutImpl extends ManagedTimeoutState {
|
||||
@Override
|
||||
protected boolean shouldPause() {
|
||||
return hasPendingWork();
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user