mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2024-11-13 05:19:59 +00:00
Merge branch 'mc-1.20.x' into mc-1.20.y
This commit is contained in:
commit
da5885ef35
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,6 +9,7 @@
|
||||
/projects/*/logs
|
||||
/projects/fabric/fabricloader.log
|
||||
/projects/*/build
|
||||
/projects/*/src/test/generated_tests/
|
||||
/buildSrc/build
|
||||
/out
|
||||
/buildSrc/out
|
||||
|
@ -102,6 +102,8 @@ sourceSets.all {
|
||||
option("NullAway:CastToNonNullMethod", "dan200.computercraft.core.util.Nullability.assertNonNull")
|
||||
option("NullAway:CheckOptionalEmptiness")
|
||||
option("NullAway:AcknowledgeRestrictiveAnnotations")
|
||||
|
||||
excludedPaths = ".*/jmh_generated/.*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ val publishCurseForge by tasks.registering(TaskPublishCurseForge::class) {
|
||||
apiToken = findProperty("curseForgeApiKey") ?: ""
|
||||
enabled = apiToken != ""
|
||||
|
||||
val mainFile = upload("282001", modPublishing.output.get().archiveFile)
|
||||
val mainFile = upload("282001", modPublishing.output)
|
||||
mainFile.changelog =
|
||||
"Release notes can be found on the [GitHub repository](https://github.com/cc-tweaked/CC-Tweaked/releases/tag/v$mcVersion-$modVersion)."
|
||||
mainFile.changelogType = "markdown"
|
||||
|
@ -13,8 +13,12 @@ SPDX-License-Identifier: MPL-2.0
|
||||
<property name="tabWidth" value="4"/>
|
||||
<property name="charset" value="UTF-8" />
|
||||
|
||||
<module name="BeforeExecutionExclusionFileFilter">
|
||||
<property name="fileNamePattern" value="module\-info\.java$"/>
|
||||
</module>
|
||||
|
||||
<module name="SuppressionFilter">
|
||||
<property name="file" value="${config_loc}/suppressions.xml" />
|
||||
<property name="file" value="${config_loc}/suppressions.xml" />
|
||||
</module>
|
||||
|
||||
<module name="BeforeExecutionExclusionFileFilter">
|
||||
|
@ -21,5 +21,5 @@ SPDX-License-Identifier: MPL-2.0
|
||||
<suppress checks="PackageName" files=".*[\\/]T[A-Za-z]+.java" />
|
||||
|
||||
<!-- Allow underscores in our test classes. -->
|
||||
<suppress checks="MethodName" files=".*Contract.java" />
|
||||
<suppress checks="MethodName" files=".*(Contract|Test).java" />
|
||||
</suppressions>
|
||||
|
@ -19,7 +19,7 @@ In order to give the best results, a GPS constellation needs at least four compu
|
||||
constellation is redundant, but it does not cause problems.
|
||||
|
||||
## Building a GPS constellation
|
||||
<img alt="An example GPS constellation." src="/images/gps-constellation-example.png" class="big-image" />
|
||||
<img alt="An example GPS constellation." src="../images/gps-constellation-example.png" class="big-image" />
|
||||
|
||||
We are going to build our GPS constellation as shown in the image above. You will need 4 computers and either 4 wireless
|
||||
modems or 4 ender modems. Try not to mix ender and wireless modems together as you might get some odd behavior when your
|
||||
|
BIN
doc/images/computercraft-dump.png
Normal file
BIN
doc/images/computercraft-dump.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 254 KiB |
BIN
doc/images/computercraft-track.png
Normal file
BIN
doc/images/computercraft-track.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 304 KiB |
140
doc/reference/command.md
Normal file
140
doc/reference/command.md
Normal file
@ -0,0 +1,140 @@
|
||||
---
|
||||
module: [kind=reference] computercraft_command
|
||||
---
|
||||
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
# The `/computercraft` command
|
||||
CC: Tweaked provides a `/computercraft` command for server owners to manage running computers on a server.
|
||||
|
||||
## Permissions {#permissions}
|
||||
As the `/computercraft` command is mostly intended for debugging and administrative purposes, its sub-commands typically
|
||||
require you to have op (or similar).
|
||||
|
||||
- All players have access to the [`queue`] sub-command.
|
||||
- On a multi-player server, all other commands require op.
|
||||
- On a single-player world, the player can run the [`dump`], [`turn-on`]/[`shutdown`], and [`track`] sub-commands, even
|
||||
when cheats are not enabled. The [`tp`] and [`view`] commands require cheats.
|
||||
|
||||
If a permission mod such as [LuckPerms] is installed[^permission], you can configure access to the individual
|
||||
sub-commands. Each sub-command creates a `computercraft.command.NAME` permission node to control which players can
|
||||
execute it.
|
||||
|
||||
[LuckPerms]: https://github.com/LuckPerms/LuckPerms/ "A permissions plugin for Minecraft servers."
|
||||
[fabric-permission-api]: https://github.com/lucko/fabric-permissions-api "A simple permissions API for Fabric"
|
||||
|
||||
[^permission]: This supports any mod which uses Forge's permission API or [fabric-permission-api].
|
||||
|
||||
## Computer selectors {#computer-selectors}
|
||||
Some commands (such as [`tp`] or [`turn-on`]) target a specific computer, or a list of computers. To specify which
|
||||
computers to operate on, you must use "computer selectors".
|
||||
|
||||
Computer selectors are similar to Minecraft's [entity target selectors], but targeting computers instead. They allow
|
||||
you to select one or more computers, based on a set of predicates.
|
||||
|
||||
The following predicates are supported:
|
||||
- `id=<id>`: Select computer(s) with a specific id.
|
||||
- `instance=<id>`: Select the computer with the given instance id.
|
||||
- `family=<normal|advanced|command>`: Select computers based on their type.
|
||||
- `label=<label>`: Select computers with the given label.
|
||||
- `distance=<distance>`: Select computers within a specific distance of the player executing the command. This uses
|
||||
Minecraft's [float range] syntax.
|
||||
|
||||
`#<id>` may also be used as a shorthand for `@c[id=<id>]`, to select computer(s) with a specific id.
|
||||
|
||||
### Examples:
|
||||
- `/computercraft turn-on #12`: Turn on the computer(s) with an id of 12.
|
||||
- `/computercraft shutdown @c[distance=..100]`: Shut down all computers with 100 blocks of the player.
|
||||
|
||||
[entity target selectors]: https://minecraft.wiki/w/Target_selectors "Target Selectors on the Minecraft wiki"
|
||||
[Float range]: https://minecraft.wiki/w/Argument_types#minecraft:float_range
|
||||
|
||||
## Commands {#commands}
|
||||
### `/computercraft dump` {#dump}
|
||||
`/computercraft dump` prints a table of currently loaded computers, including their id, position, and whether they're
|
||||
running. It can also be run with a single computer argument to dump more detailed information about a computer.
|
||||
|
||||
![A screenshot of a Minecraft world. In the chat box, there is a table listing 5 computers, with columns labelled
|
||||
"Computer", "On" and "Position". Below that, is a more detailed list of information about Computer 0, including its
|
||||
label ("My computer") and that it has a monitor on the right hand side](../images/computercraft-dump.png "An example of
|
||||
running '/computercraft dump'")
|
||||
|
||||
Next to the computer id, there are several buttons to either [teleport][`tp`] to the computer, or [open its terminal
|
||||
][`view`].
|
||||
|
||||
Computers are sorted by distance to the player, so nearby computers will appear earlier.
|
||||
|
||||
### `/computercraft turn-on [computers...]` {#turn-on}
|
||||
Turn on one or more computers or, if no run with no arguments, all loaded computers.
|
||||
|
||||
#### Examples
|
||||
- `/computercraft turn-on #0 #2`: Turn on computers with id 0 and 2.
|
||||
- `/computercraft turn-on @c[family=command]`: Turn on all command computers.
|
||||
|
||||
### `/computercraft shutdown [computers...]` {#shutdown}
|
||||
Shutdown one or more computers or, if no run with no arguments, all loaded computers.
|
||||
|
||||
This is sometimes useful when dealing with lag, as a way to ensure that ComputerCraft is not causing problems.
|
||||
|
||||
#### Examples
|
||||
- `/computercraft shutdown`: Shut down all loaded computers.
|
||||
- `/computercraft shutdown @c[distance=..10]`: Shut down all computers in a block radius.
|
||||
|
||||
### `/computercraft tp [computer]` {#tp}
|
||||
Teleport to the given computer.
|
||||
|
||||
This is normally used from via the [`dump`] command interface rather than being invoked directly.
|
||||
|
||||
### `/computercraft view [computer]` {#view}
|
||||
Open a terminal for the specified computer. This allows remotely viewing computers without having to interact with the
|
||||
block.
|
||||
|
||||
This is normally used from via the [`dump`] command interface rather than being invoked directly.
|
||||
|
||||
### `/computercraft track` {#track}
|
||||
The `/computercraft track` command allows you to enable profiling of computers. When a computer runs code, or interacts
|
||||
with the Minecraft world, we time how long that takes. This timing information may then be queried, and used to find
|
||||
computers which may be causing lag.
|
||||
|
||||
To enable the profiler, run `/computercraft track start`. Computers will then start recording metrics. Once enough data
|
||||
has been gathered, run `/computercraft track stop` to stop profiling and display the recorded data.
|
||||
|
||||
![](../images/computercraft-track.png)
|
||||
|
||||
The table by default shows the number of times each computer has run, and how long it ran for (in total, and on
|
||||
average). In the above screenshot, we can see one computer was particularly badly behaved, and ran for 7 seconds. The
|
||||
buttons may be used to [teleport][`tp`] to the computer, or [open its terminal ][`view`], and inspect it further.
|
||||
|
||||
`/computercraft track dump` can be used to display this table at any point (including while profiling is still running).
|
||||
|
||||
Computers also record other information, such as how much server-thread time they consume, or their HTTP bandwidth
|
||||
usage. The `dump` subcommand accepts a list of other fields to display, instead of the default timings.
|
||||
|
||||
#### Examples
|
||||
- `/computercraft track dump server_tasks_count server_tasks`: Print the number of server-thread tasks each computer
|
||||
executed, and how long they took in total.
|
||||
- `/computercraft track dump http_upload http_download`: Print the number of bytes uploaded and downloaded by each
|
||||
computer.
|
||||
|
||||
|
||||
### `/computercraft queue` {#queue}
|
||||
The queue subcommand allows non-operator players to queue a `computer_command` event on *command* computers.
|
||||
|
||||
This has a similar purpose to vanilla's [`/trigger`] command. Command computers may choose to listen to this event, and
|
||||
then perform some action.
|
||||
|
||||
[`/trigger`]: https://minecraft.wiki/w/Commands/trigger "/trigger on the Minecraft wiki"
|
||||
|
||||
|
||||
[`dump`]: #dump "/computercraft dump"
|
||||
[`queue`]: #queue "/computercraft queue"
|
||||
[`shutdown`]: #shutdown "/computercraft shutdown"
|
||||
[`tp`]: #tp "/computercraft tp"
|
||||
[`track`]: #track "/computercraft track"
|
||||
[`turn-on`]: #turn-on "/computercraft turn-on"
|
||||
[`view`]: #view "/computercraft view"
|
||||
[computer selectors]: #computer-selectors "Computer selectors"
|
@ -10,7 +10,7 @@ kotlin.jvm.target.validation.mode=error
|
||||
|
||||
# Mod properties
|
||||
isUnstable=true
|
||||
modVersion=1.109.6
|
||||
modVersion=1.110.0
|
||||
|
||||
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
|
||||
mcVersion=1.20.4
|
||||
|
@ -9,7 +9,7 @@
|
||||
# Remember to update corresponding versions in fabric.mod.json/mods.toml
|
||||
fabric-api = "0.93.1+1.20.4"
|
||||
fabric-loader = "0.15.3"
|
||||
neoForge = "20.4.161-beta"
|
||||
neoForge = "20.4.210"
|
||||
neoForgeSpi = "8.0.1"
|
||||
mixin = "0.8.5"
|
||||
parchment = "2023.12.31"
|
||||
@ -26,7 +26,7 @@ slf4j = "2.0.7"
|
||||
asm = "9.6"
|
||||
autoService = "1.1.1"
|
||||
checkerFramework = "3.42.0"
|
||||
cobalt = "0.9.1"
|
||||
cobalt = "0.9.2"
|
||||
commonsCli = "1.6.0"
|
||||
jetbrainsAnnotations = "24.1.0"
|
||||
jsr305 = "3.0.2"
|
||||
@ -51,10 +51,11 @@ sodium = "mc1.20-0.4.10"
|
||||
hamcrest = "2.2"
|
||||
jqwik = "1.8.2"
|
||||
junit = "5.10.1"
|
||||
jmh = "1.37"
|
||||
|
||||
# Build tools
|
||||
cctJavadoc = "1.8.2"
|
||||
checkstyle = "10.12.6"
|
||||
checkstyle = "10.14.1"
|
||||
curseForgeGradle = "1.1.18"
|
||||
errorProne-core = "2.23.0"
|
||||
errorProne-plugin = "3.1.0"
|
||||
@ -62,15 +63,15 @@ fabric-loom = "1.5.7"
|
||||
githubRelease = "2.5.2"
|
||||
gradleVersions = "0.50.0"
|
||||
ideaExt = "1.1.7"
|
||||
illuaminate = "0.1.0-44-g9ee0055"
|
||||
illuaminate = "0.1.0-69-gf294ab2"
|
||||
lwjgl = "3.3.3"
|
||||
minotaur = "2.8.7"
|
||||
neoGradle = "7.0.93"
|
||||
neoGradle = "7.0.100"
|
||||
nullAway = "0.9.9"
|
||||
spotless = "6.23.3"
|
||||
taskTree = "2.1.1"
|
||||
teavm = "0.10.0-SQUID.2"
|
||||
vanillaExtract = "0.1.1"
|
||||
teavm = "0.10.0-SQUID.3"
|
||||
vanillaExtract = "0.1.2"
|
||||
versionCatalogUpdate = "0.8.1"
|
||||
|
||||
[libraries]
|
||||
@ -125,6 +126,8 @@ junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.re
|
||||
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }
|
||||
junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" }
|
||||
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
|
||||
jmh = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" }
|
||||
jmh-processor = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" }
|
||||
|
||||
# LWJGL
|
||||
lwjgl-bom = { module = "org.lwjgl:lwjgl-bom", version.ref = "lwjgl" }
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
20
gradlew.bat
vendored
20
gradlew.bat
vendored
@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
package dan200.computercraft.api.network.wired;
|
||||
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@ -22,6 +23,7 @@ import java.util.Map;
|
||||
*
|
||||
* @see WiredNode#getNetwork()
|
||||
*/
|
||||
@ApiStatus.NonExtendable
|
||||
public interface WiredNetwork {
|
||||
/**
|
||||
* Create a connection between two nodes.
|
||||
@ -35,7 +37,9 @@ public interface WiredNetwork {
|
||||
* @throws IllegalArgumentException If {@code left} and {@code right} are equal.
|
||||
* @see WiredNode#connectTo(WiredNode)
|
||||
* @see WiredNetwork#connect(WiredNode, WiredNode)
|
||||
* @deprecated Use {@link WiredNode#connectTo(WiredNode)}
|
||||
*/
|
||||
@Deprecated
|
||||
boolean connect(WiredNode left, WiredNode right);
|
||||
|
||||
/**
|
||||
@ -50,7 +54,9 @@ public interface WiredNetwork {
|
||||
* @throws IllegalArgumentException If {@code left} and {@code right} are equal.
|
||||
* @see WiredNode#disconnectFrom(WiredNode)
|
||||
* @see WiredNetwork#connect(WiredNode, WiredNode)
|
||||
* @deprecated Use {@link WiredNode#disconnectFrom(WiredNode)}
|
||||
*/
|
||||
@Deprecated
|
||||
boolean disconnect(WiredNode left, WiredNode right);
|
||||
|
||||
/**
|
||||
@ -64,7 +70,9 @@ public interface WiredNetwork {
|
||||
* only element.
|
||||
* @throws IllegalArgumentException If the node is not in the network.
|
||||
* @see WiredNode#remove()
|
||||
* @deprecated Use {@link WiredNode#remove()}
|
||||
*/
|
||||
@Deprecated
|
||||
boolean remove(WiredNode node);
|
||||
|
||||
/**
|
||||
@ -77,6 +85,8 @@ public interface WiredNetwork {
|
||||
* @param peripherals The new peripherals for this node.
|
||||
* @throws IllegalArgumentException If the node is not in the network.
|
||||
* @see WiredNode#updatePeripherals(Map)
|
||||
* @deprecated Use {@link WiredNode#updatePeripherals(Map)}
|
||||
*/
|
||||
@Deprecated
|
||||
void updatePeripherals(WiredNode node, Map<String, IPeripheral> peripherals);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ package dan200.computercraft.api.network.wired;
|
||||
|
||||
import dan200.computercraft.api.network.PacketNetwork;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@ -22,6 +23,7 @@ import java.util.Map;
|
||||
* Wired nodes also provide several convenience methods for interacting with a wired network. These should only ever
|
||||
* be used on the main server thread.
|
||||
*/
|
||||
@ApiStatus.NonExtendable
|
||||
public interface WiredNode extends PacketNetwork {
|
||||
/**
|
||||
* The associated element for this network node.
|
||||
@ -37,7 +39,9 @@ public interface WiredNode extends PacketNetwork {
|
||||
* This should only be used on the server thread.
|
||||
*
|
||||
* @return This node's network.
|
||||
* @deprecated Use the connect/disconnect/remove methods on {@link WiredNode}.
|
||||
*/
|
||||
@Deprecated
|
||||
WiredNetwork getNetwork();
|
||||
|
||||
/**
|
||||
@ -47,12 +51,9 @@ public interface WiredNode extends PacketNetwork {
|
||||
*
|
||||
* @param node The other node to connect to.
|
||||
* @return {@code true} if a connection was created or {@code false} if the connection already exists.
|
||||
* @see WiredNetwork#connect(WiredNode, WiredNode)
|
||||
* @see WiredNode#disconnectFrom(WiredNode)
|
||||
*/
|
||||
default boolean connectTo(WiredNode node) {
|
||||
return getNetwork().connect(this, node);
|
||||
}
|
||||
boolean connectTo(WiredNode node);
|
||||
|
||||
/**
|
||||
* Destroy a connection between this node and another.
|
||||
@ -61,13 +62,9 @@ public interface WiredNode extends PacketNetwork {
|
||||
*
|
||||
* @param node The other node to disconnect from.
|
||||
* @return {@code true} if a connection was destroyed or {@code false} if no connection exists.
|
||||
* @throws IllegalArgumentException If {@code node} is not on the same network.
|
||||
* @see WiredNetwork#disconnect(WiredNode, WiredNode)
|
||||
* @see WiredNode#connectTo(WiredNode)
|
||||
*/
|
||||
default boolean disconnectFrom(WiredNode node) {
|
||||
return getNetwork().disconnect(this, node);
|
||||
}
|
||||
boolean disconnectFrom(WiredNode node);
|
||||
|
||||
/**
|
||||
* Sever all connections this node has, removing it from this network.
|
||||
@ -78,11 +75,8 @@ public interface WiredNode extends PacketNetwork {
|
||||
* @return Whether this node was removed from the network. One cannot remove a node from a network where it is the
|
||||
* only element.
|
||||
* @throws IllegalArgumentException If the node is not in the network.
|
||||
* @see WiredNetwork#remove(WiredNode)
|
||||
*/
|
||||
default boolean remove() {
|
||||
return getNetwork().remove(this);
|
||||
}
|
||||
boolean remove();
|
||||
|
||||
/**
|
||||
* Mark this node's peripherals as having changed.
|
||||
@ -91,9 +85,6 @@ public interface WiredNode extends PacketNetwork {
|
||||
* that your network element owns.
|
||||
*
|
||||
* @param peripherals The new peripherals for this node.
|
||||
* @see WiredNetwork#updatePeripherals(WiredNode, Map)
|
||||
*/
|
||||
default void updatePeripherals(Map<String, IPeripheral> peripherals) {
|
||||
getNetwork().updatePeripherals(this, peripherals);
|
||||
}
|
||||
void updatePeripherals(Map<String, IPeripheral> peripherals);
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ public final class Services {
|
||||
* @throws IllegalStateException When the service cannot be loaded.
|
||||
*/
|
||||
public static <T> T load(Class<T> klass) {
|
||||
var services = ServiceLoader.load(klass).stream().toList();
|
||||
var services = ServiceLoader.load(klass, klass.getClassLoader()).stream().toList();
|
||||
return switch (services.size()) {
|
||||
case 1 -> services.get(0).get();
|
||||
case 0 -> throw new IllegalStateException("Cannot find service for " + klass.getName());
|
||||
|
@ -46,6 +46,9 @@ dependencies {
|
||||
testImplementation(libs.bundles.test)
|
||||
testRuntimeOnly(libs.bundles.testRuntime)
|
||||
|
||||
testImplementation(libs.jmh)
|
||||
testAnnotationProcessor(libs.jmh.processor)
|
||||
|
||||
testModCompileOnly(libs.mixin)
|
||||
testModImplementation(testFixtures(project(":core")))
|
||||
testModImplementation(testFixtures(project(":common")))
|
||||
|
@ -22,6 +22,7 @@ import dan200.computercraft.core.util.Colour;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.command.CommandComputerCraft;
|
||||
import dan200.computercraft.shared.common.IColouredItem;
|
||||
import dan200.computercraft.shared.computer.core.ComputerState;
|
||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
|
||||
import dan200.computercraft.shared.computer.inventory.ViewComputerMenu;
|
||||
@ -81,13 +82,18 @@ public final class ClientRegistry {
|
||||
|
||||
/**
|
||||
* Register any client-side objects which must be done on the main thread.
|
||||
*
|
||||
* @param itemProperties Callback to register item properties.
|
||||
*/
|
||||
public static void registerMainThread() {
|
||||
registerItemProperty("state",
|
||||
new UnclampedPropertyFunction((stack, world, player, random) -> ClientPocketComputers.get(stack).getState().ordinal()),
|
||||
public static void registerMainThread(RegisterItemProperty itemProperties) {
|
||||
registerItemProperty(itemProperties, "state",
|
||||
new UnclampedPropertyFunction((stack, world, player, random) -> {
|
||||
var computer = ClientPocketComputers.get(stack);
|
||||
return (computer == null ? ComputerState.OFF : computer.getState()).ordinal();
|
||||
}),
|
||||
ModRegistry.Items.POCKET_COMPUTER_NORMAL, ModRegistry.Items.POCKET_COMPUTER_ADVANCED
|
||||
);
|
||||
registerItemProperty("coloured",
|
||||
registerItemProperty(itemProperties, "coloured",
|
||||
(stack, world, player, random) -> IColouredItem.getColourBasic(stack) != -1 ? 1 : 0,
|
||||
ModRegistry.Items.POCKET_COMPUTER_NORMAL, ModRegistry.Items.POCKET_COMPUTER_ADVANCED
|
||||
);
|
||||
@ -125,9 +131,17 @@ public final class ClientRegistry {
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
private static void registerItemProperty(String name, ClampedItemPropertyFunction getter, Supplier<? extends Item>... items) {
|
||||
private static void registerItemProperty(RegisterItemProperty itemProperties, String name, ClampedItemPropertyFunction getter, Supplier<? extends Item>... items) {
|
||||
var id = new ResourceLocation(ComputerCraftAPI.MOD_ID, name);
|
||||
for (var item : items) ItemProperties.register(item.get(), id, getter);
|
||||
for (var item : items) itemProperties.register(item.get(), id, getter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an item property via {@link ItemProperties#register}. Forge and Fabric expose different methods, so we
|
||||
* supply this via mod-loader-specific code.
|
||||
*/
|
||||
public interface RegisterItemProperty {
|
||||
void register(Item item, ResourceLocation name, ClampedItemPropertyFunction property);
|
||||
}
|
||||
|
||||
public static void registerReloadListeners(Consumer<PreparableReloadListener> register, Minecraft minecraft) {
|
||||
@ -165,17 +179,14 @@ public final class ClientRegistry {
|
||||
}
|
||||
|
||||
private static int getPocketColour(ItemStack stack, int layer) {
|
||||
switch (layer) {
|
||||
case 0:
|
||||
default:
|
||||
return 0xFFFFFF;
|
||||
case 1: // Frame colour
|
||||
return IColouredItem.getColourBasic(stack);
|
||||
case 2: { // Light colour
|
||||
var light = ClientPocketComputers.get(stack).getLightState();
|
||||
return light == -1 ? Colour.BLACK.getHex() : light;
|
||||
return switch (layer) {
|
||||
default -> 0xFFFFFF;
|
||||
case 1 -> IColouredItem.getColourBasic(stack); // Frame colour
|
||||
case 2 -> { // Light colour
|
||||
var computer = ClientPocketComputers.get(stack);
|
||||
yield computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static int getTurtleColour(ItemStack stack, int layer) {
|
||||
|
@ -17,6 +17,7 @@ import dan200.computercraft.shared.computer.terminal.TerminalState;
|
||||
import dan200.computercraft.shared.computer.upload.UploadResult;
|
||||
import dan200.computercraft.shared.network.client.ClientNetworkContext;
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity;
|
||||
import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
|
||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
@ -27,7 +28,6 @@ import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
@ -67,19 +67,17 @@ public final class ClientNetworkContextImpl implements ClientNetworkContext {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePocketComputerData(int instanceId, ComputerState state, int lightState, TerminalState terminal) {
|
||||
var computer = ClientPocketComputers.get(instanceId, terminal.colour);
|
||||
computer.setState(state, lightState);
|
||||
if (terminal.hasTerminal()) computer.setTerminal(terminal);
|
||||
public void handlePocketComputerData(UUID instanceId, ComputerState state, int lightState, TerminalState terminal) {
|
||||
ClientPocketComputers.setState(instanceId, state, lightState, terminal);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePocketComputerDeleted(int instanceId) {
|
||||
public void handlePocketComputerDeleted(UUID instanceId) {
|
||||
ClientPocketComputers.remove(instanceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, ByteBuffer buffer) {
|
||||
public void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, EncodedAudio buffer) {
|
||||
SpeakerManager.getSound(source).playAudio(reifyPosition(position), volume, buffer);
|
||||
}
|
||||
|
||||
|
@ -4,21 +4,26 @@
|
||||
|
||||
package dan200.computercraft.client.pocket;
|
||||
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ComputerState;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
|
||||
import dan200.computercraft.shared.computer.terminal.TerminalState;
|
||||
import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Maps {@link ServerComputer#getInstanceID()} to locals {@link PocketComputerData}.
|
||||
* Maps {@link ServerComputer#getInstanceUUID()} to locals {@link PocketComputerData}.
|
||||
* <p>
|
||||
* This is populated by {@link PocketComputerDataMessage} and accessed when rendering pocket computers
|
||||
*/
|
||||
public final class ClientPocketComputers {
|
||||
private static final Int2ObjectMap<PocketComputerData> instances = new Int2ObjectOpenHashMap<>();
|
||||
private static final Map<UUID, PocketComputerData> instances = new HashMap<>();
|
||||
|
||||
private ClientPocketComputers() {
|
||||
}
|
||||
@ -27,25 +32,32 @@ public final class ClientPocketComputers {
|
||||
instances.clear();
|
||||
}
|
||||
|
||||
public static void remove(int id) {
|
||||
public static void remove(UUID id) {
|
||||
instances.remove(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create a pocket computer.
|
||||
* Set the state of a pocket computer.
|
||||
*
|
||||
* @param instanceId The instance ID of the pocket computer.
|
||||
* @param advanced Whether this computer has an advanced terminal.
|
||||
* @return The pocket computer data.
|
||||
* @param instanceId The instance ID of the pocket computer.
|
||||
* @param state The computer state of the pocket computer.
|
||||
* @param lightColour The current colour of the modem light.
|
||||
* @param terminalData The current terminal contents.
|
||||
*/
|
||||
public static PocketComputerData get(int instanceId, boolean advanced) {
|
||||
public static void setState(UUID instanceId, ComputerState state, int lightColour, TerminalState terminalData) {
|
||||
var computer = instances.get(instanceId);
|
||||
if (computer == null) instances.put(instanceId, computer = new PocketComputerData(advanced));
|
||||
return computer;
|
||||
if (computer == null) {
|
||||
var terminal = new NetworkedTerminal(terminalData.width, terminalData.height, terminalData.colour);
|
||||
instances.put(instanceId, computer = new PocketComputerData(state, lightColour, terminal));
|
||||
} else {
|
||||
computer.setState(state, lightColour);
|
||||
}
|
||||
|
||||
if (terminalData.hasTerminal()) terminalData.apply(computer.getTerminal());
|
||||
}
|
||||
|
||||
public static PocketComputerData get(ItemStack stack) {
|
||||
var family = stack.getItem() instanceof PocketComputerItem computer ? computer.getFamily() : ComputerFamily.NORMAL;
|
||||
return get(PocketComputerItem.getInstanceID(stack), family != ComputerFamily.NORMAL);
|
||||
public static @Nullable PocketComputerData get(ItemStack stack) {
|
||||
var id = PocketComputerItem.getInstanceID(stack);
|
||||
return id == null ? null : instances.get(id);
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,8 @@
|
||||
|
||||
package dan200.computercraft.client.pocket;
|
||||
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import dan200.computercraft.shared.computer.core.ComputerState;
|
||||
import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
|
||||
import dan200.computercraft.shared.computer.terminal.TerminalState;
|
||||
import dan200.computercraft.shared.config.Config;
|
||||
import dan200.computercraft.shared.pocket.core.PocketServerComputer;
|
||||
|
||||
/**
|
||||
@ -21,20 +18,22 @@ import dan200.computercraft.shared.pocket.core.PocketServerComputer;
|
||||
* @see ClientPocketComputers The registry which holds pocket computers.
|
||||
* @see PocketServerComputer The server-side pocket computer.
|
||||
*/
|
||||
public class PocketComputerData {
|
||||
public final class PocketComputerData {
|
||||
private final NetworkedTerminal terminal;
|
||||
private ComputerState state = ComputerState.OFF;
|
||||
private int lightColour = -1;
|
||||
private ComputerState state;
|
||||
private int lightColour;
|
||||
|
||||
public PocketComputerData(boolean colour) {
|
||||
terminal = new NetworkedTerminal(Config.pocketTermWidth, Config.pocketTermHeight, colour);
|
||||
PocketComputerData(ComputerState state, int lightColour, NetworkedTerminal terminal) {
|
||||
this.state = state;
|
||||
this.lightColour = lightColour;
|
||||
this.terminal = terminal;
|
||||
}
|
||||
|
||||
public int getLightState() {
|
||||
return state != ComputerState.OFF ? lightColour : -1;
|
||||
}
|
||||
|
||||
public Terminal getTerminal() {
|
||||
public NetworkedTerminal getTerminal() {
|
||||
return terminal;
|
||||
}
|
||||
|
||||
@ -42,12 +41,8 @@ public class PocketComputerData {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(ComputerState state, int lightColour) {
|
||||
void setState(ComputerState state, int lightColour) {
|
||||
this.state = state;
|
||||
this.lightColour = lightColour;
|
||||
}
|
||||
|
||||
public void setTerminal(TerminalState state) {
|
||||
state.apply(terminal);
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import dan200.computercraft.client.pocket.ClientPocketComputers;
|
||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.config.Config;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
@ -32,10 +33,16 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
|
||||
@Override
|
||||
protected void renderItem(PoseStack transform, MultiBufferSource bufferSource, ItemStack stack, int light) {
|
||||
var computer = ClientPocketComputers.get(stack);
|
||||
var terminal = computer.getTerminal();
|
||||
var terminal = computer == null ? null : computer.getTerminal();
|
||||
|
||||
var termWidth = terminal.getWidth();
|
||||
var termHeight = terminal.getHeight();
|
||||
int termWidth, termHeight;
|
||||
if (terminal == null) {
|
||||
termWidth = Config.pocketTermWidth;
|
||||
termHeight = Config.pocketTermHeight;
|
||||
} else {
|
||||
termWidth = terminal.getWidth();
|
||||
termHeight = terminal.getHeight();
|
||||
}
|
||||
|
||||
var width = termWidth * FONT_WIDTH + MARGIN * 2;
|
||||
var height = termHeight * FONT_HEIGHT + MARGIN * 2;
|
||||
@ -60,14 +67,15 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
|
||||
renderFrame(matrix, bufferSource, family, frameColour, light, width, height);
|
||||
|
||||
// Render the light
|
||||
var lightColour = ClientPocketComputers.get(stack).getLightState();
|
||||
if (lightColour == -1) lightColour = Colour.BLACK.getHex();
|
||||
var lightColour = computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState();
|
||||
renderLight(transform, bufferSource, lightColour, width, height);
|
||||
|
||||
FixedWidthFontRenderer.drawTerminal(
|
||||
FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL)),
|
||||
MARGIN, MARGIN, terminal, MARGIN, MARGIN, MARGIN, MARGIN
|
||||
);
|
||||
var quadEmitter = FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL));
|
||||
if (terminal == null) {
|
||||
FixedWidthFontRenderer.drawEmptyTerminal(quadEmitter, 0, 0, width, height);
|
||||
} else {
|
||||
FixedWidthFontRenderer.drawTerminal(quadEmitter, MARGIN, MARGIN, terminal, MARGIN, MARGIN, MARGIN, MARGIN);
|
||||
}
|
||||
|
||||
transform.popPose();
|
||||
}
|
||||
|
@ -60,9 +60,9 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
|
||||
@Override
|
||||
public void render(MonitorBlockEntity monitor, float partialTicks, PoseStack transform, MultiBufferSource bufferSource, int lightmapCoord, int overlayLight) {
|
||||
// Render from the origin monitor
|
||||
var originTerminal = monitor.getClientMonitor();
|
||||
|
||||
var originTerminal = monitor.getOriginClientMonitor();
|
||||
if (originTerminal == null) return;
|
||||
|
||||
var origin = originTerminal.getOrigin();
|
||||
var renderState = originTerminal.getRenderState(MonitorRenderState::new);
|
||||
var monitorPos = monitor.getBlockPos();
|
||||
|
@ -5,6 +5,7 @@
|
||||
package dan200.computercraft.client.sound;
|
||||
|
||||
import com.mojang.blaze3d.audio.Channel;
|
||||
import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
|
||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral;
|
||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
|
||||
import net.minecraft.client.sounds.AudioStream;
|
||||
@ -36,7 +37,7 @@ class DfpwmStream implements AudioStream {
|
||||
/**
|
||||
* The {@link Channel} which this sound is playing on.
|
||||
*
|
||||
* @see SpeakerInstance#playAudio(SpeakerPosition, float, ByteBuffer)
|
||||
* @see SpeakerInstance#playAudio(SpeakerPosition, float, EncodedAudio)
|
||||
*/
|
||||
@Nullable
|
||||
Channel channel;
|
||||
@ -44,21 +45,23 @@ class DfpwmStream implements AudioStream {
|
||||
/**
|
||||
* The underlying {@link SoundEngine} executor.
|
||||
*
|
||||
* @see SpeakerInstance#playAudio(SpeakerPosition, float, ByteBuffer)
|
||||
* @see SpeakerInstance#playAudio(SpeakerPosition, float, EncodedAudio)
|
||||
* @see SoundEngine#executor
|
||||
*/
|
||||
@Nullable
|
||||
Executor executor;
|
||||
|
||||
private int charge = 0; // q
|
||||
private int strength = 0; // s
|
||||
private int lowPassCharge;
|
||||
private boolean previousBit = false;
|
||||
|
||||
DfpwmStream() {
|
||||
}
|
||||
|
||||
void push(ByteBuffer input) {
|
||||
void push(EncodedAudio audio) {
|
||||
var charge = audio.charge();
|
||||
var strength = audio.strength();
|
||||
var previousBit = audio.previousBit();
|
||||
var input = audio.audio();
|
||||
|
||||
var readable = input.remaining();
|
||||
var output = ByteBuffer.allocate(readable * 8).order(ByteOrder.nativeOrder());
|
||||
|
||||
|
@ -6,12 +6,12 @@ package dan200.computercraft.client.sound;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.core.util.Nullability;
|
||||
import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
|
||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* An instance of a speaker, which is either playing a {@link DfpwmStream} stream or a normal sound.
|
||||
@ -25,7 +25,7 @@ public class SpeakerInstance {
|
||||
SpeakerInstance() {
|
||||
}
|
||||
|
||||
private void pushAudio(ByteBuffer buffer) {
|
||||
private void pushAudio(EncodedAudio buffer) {
|
||||
var sound = this.sound;
|
||||
|
||||
var stream = currentStream;
|
||||
@ -43,7 +43,7 @@ public class SpeakerInstance {
|
||||
}
|
||||
}
|
||||
|
||||
public void playAudio(SpeakerPosition position, float volume, ByteBuffer buffer) {
|
||||
public void playAudio(SpeakerPosition position, float volume, EncodedAudio buffer) {
|
||||
pushAudio(buffer);
|
||||
|
||||
var soundManager = Minecraft.getInstance().getSoundManager();
|
||||
|
@ -13,6 +13,7 @@ import dan200.computercraft.api.upgrades.UpgradeBase;
|
||||
import dan200.computercraft.core.metrics.Metric;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.command.arguments.ComputerSelector;
|
||||
import dan200.computercraft.shared.computer.metrics.basic.Aggregate;
|
||||
import dan200.computercraft.shared.computer.metrics.basic.AggregatedMetric;
|
||||
import dan200.computercraft.shared.config.ConfigFile;
|
||||
@ -165,10 +166,19 @@ public final class LanguageProvider implements DataProvider {
|
||||
add("commands.computercraft.generic.exception", "Unhandled exception (%s)");
|
||||
add("commands.computercraft.generic.additional_rows", "%d additional rows…");
|
||||
|
||||
// Argument types
|
||||
add("argument.computercraft.computer.instance", "Unique instance ID");
|
||||
add("argument.computercraft.computer.id", "Computer ID");
|
||||
add("argument.computercraft.computer.label", "Computer label");
|
||||
add("argument.computercraft.computer.distance", "Distance to entity");
|
||||
add("argument.computercraft.computer.family", "Computer family");
|
||||
|
||||
// Exceptions
|
||||
add("argument.computercraft.computer.no_matching", "No computers matching '%s'");
|
||||
add("argument.computercraft.computer.many_matching", "Multiple computers matching '%s' (instances %s)");
|
||||
add("argument.computercraft.tracking_field.no_field", "Unknown field '%s'");
|
||||
add("argument.computercraft.argument_expected", "Argument expected");
|
||||
add("argument.computercraft.unknown_computer_family", "Unknown computer family '%s'");
|
||||
|
||||
// Metrics
|
||||
add(Metrics.COMPUTER_TASKS, "Tasks");
|
||||
@ -281,7 +291,8 @@ public final class LanguageProvider implements DataProvider {
|
||||
pocketUpgrades.getGeneratedUpgrades().stream().map(UpgradeBase::getUnlocalisedAdjective),
|
||||
Metric.metrics().values().stream().map(x -> AggregatedMetric.TRANSLATION_PREFIX + x.name() + ".name"),
|
||||
ConfigSpec.serverSpec.entries().map(ConfigFile.Entry::translationKey),
|
||||
ConfigSpec.clientSpec.entries().map(ConfigFile.Entry::translationKey)
|
||||
ConfigSpec.clientSpec.entries().map(ConfigFile.Entry::translationKey),
|
||||
ComputerSelector.options().values().stream().map(ComputerSelector.Option::translationKey)
|
||||
).flatMap(x -> x);
|
||||
}
|
||||
|
||||
|
@ -4,45 +4,66 @@
|
||||
|
||||
package dan200.computercraft.impl.network.wired;
|
||||
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Verifies certain elements of a network are "well formed".
|
||||
* Verifies certain elements of a network are well-formed.
|
||||
* <p>
|
||||
* This adds substantial overhead to network modification, and so should only be enabled
|
||||
* in a development environment.
|
||||
* This adds substantial overhead to network modification, and so is only enabled when assertions are enabled.
|
||||
*/
|
||||
public final class InvariantChecker {
|
||||
final class InvariantChecker {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(InvariantChecker.class);
|
||||
private static final boolean ENABLED = false;
|
||||
|
||||
private InvariantChecker() {
|
||||
}
|
||||
|
||||
public static void checkNode(WiredNodeImpl node) {
|
||||
if (!ENABLED) return;
|
||||
static void checkNode(WiredNodeImpl node) {
|
||||
assert checkNodeImpl(node) : "Node invariants failed. See logs.";
|
||||
}
|
||||
|
||||
var network = node.network;
|
||||
if (network == null) {
|
||||
LOG.error("Node's network is null", new Exception());
|
||||
return;
|
||||
private static boolean checkNodeImpl(WiredNodeImpl node) {
|
||||
var okay = true;
|
||||
|
||||
if (node.currentSet != null) {
|
||||
okay = false;
|
||||
LOG.error("{}: currentSet was not cleared.", node);
|
||||
}
|
||||
|
||||
if (network.nodes == null || !network.nodes.contains(node)) {
|
||||
LOG.error("Node's network does not contain node", new Exception());
|
||||
var network = makeNullable(node.network);
|
||||
if (network == null) {
|
||||
okay = false;
|
||||
LOG.error("{}: Node's network is null.", node);
|
||||
} else if (makeNullable(network.nodes) == null || !network.nodes.contains(node)) {
|
||||
okay = false;
|
||||
LOG.error("{}: Node's network does not contain node.", node);
|
||||
}
|
||||
|
||||
for (var neighbour : node.neighbours) {
|
||||
if (!neighbour.neighbours.contains(node)) {
|
||||
LOG.error("Neighbour is missing node", new Exception());
|
||||
okay = false;
|
||||
LOG.error("{}: Neighbour {}'s neighbour set does not contain origianl node.", node, neighbour);
|
||||
}
|
||||
}
|
||||
|
||||
return okay;
|
||||
}
|
||||
|
||||
public static void checkNetwork(WiredNetworkImpl network) {
|
||||
if (!ENABLED) return;
|
||||
static void checkNetwork(WiredNetworkImpl network) {
|
||||
assert checkNetworkImpl(network) : "Network invariants failed. See logs.";
|
||||
}
|
||||
|
||||
for (var node : network.nodes) checkNode(node);
|
||||
private static boolean checkNetworkImpl(WiredNetworkImpl network) {
|
||||
var okay = true;
|
||||
for (var node : network.nodes) okay &= checkNodeImpl(node);
|
||||
return okay;
|
||||
}
|
||||
|
||||
@Contract("")
|
||||
private static <T> @Nullable T makeNullable(T object) {
|
||||
return object;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,100 @@
|
||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.impl.network.wired;
|
||||
|
||||
import dan200.computercraft.api.network.wired.WiredNode;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A disjoint-set/union-find of {@link WiredNodeImpl}s.
|
||||
* <p>
|
||||
* Rather than actually maintaining a list of included nodes, wired nodes store {@linkplain WiredNodeImpl#currentSet the
|
||||
* set they're part of}. This means that we can only have one disjoint-set at once, but that is not a problem in
|
||||
* practice.
|
||||
*
|
||||
* @see WiredNodeImpl#currentSet
|
||||
* @see WiredNetworkImpl#remove(WiredNode)
|
||||
* @see <a href="https://en.wikipedia.org/wiki/Disjoint-set_data_structure">Disjoint-set data structure</a>
|
||||
*/
|
||||
class NodeSet {
|
||||
private NodeSet parent = this;
|
||||
private int size = 1;
|
||||
private @Nullable WiredNetworkImpl network;
|
||||
|
||||
private boolean isRoot() {
|
||||
return parent == this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve this union, finding the root {@link NodeSet}.
|
||||
*
|
||||
* @return The root union.
|
||||
*/
|
||||
NodeSet find() {
|
||||
var self = this;
|
||||
while (!self.isRoot()) self = self.parent = self.parent.parent;
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of this node set.
|
||||
*
|
||||
* @return The size of the set.
|
||||
*/
|
||||
int size() {
|
||||
return find().size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a node to this {@link NodeSet}.
|
||||
*
|
||||
* @param node The node to add to the set.
|
||||
*/
|
||||
void addNode(WiredNodeImpl node) {
|
||||
if (!isRoot()) throw new IllegalStateException("Cannot grow a non-root set.");
|
||||
if (node.currentSet != null) throw new IllegalArgumentException("Node is already in a set.");
|
||||
|
||||
node.currentSet = this;
|
||||
size++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two nodes sets together.
|
||||
*
|
||||
* @param left The first union.
|
||||
* @param right The second union.
|
||||
* @return The union which was subsumed.
|
||||
*/
|
||||
public static NodeSet merge(NodeSet left, NodeSet right) {
|
||||
if (!left.isRoot() || !right.isRoot()) throw new IllegalArgumentException("Cannot union a non-root set.");
|
||||
if (left == right) throw new IllegalArgumentException("Cannot merge a node into itself.");
|
||||
|
||||
return left.size >= right.size ? mergeInto(left, right) : mergeInto(right, left);
|
||||
}
|
||||
|
||||
private static NodeSet mergeInto(NodeSet root, NodeSet child) {
|
||||
assert root.size > child.size;
|
||||
child.parent = root;
|
||||
root.size += child.size;
|
||||
return child;
|
||||
}
|
||||
|
||||
void setNetwork(WiredNetworkImpl network) {
|
||||
if (!isRoot()) throw new IllegalStateException("Set is not the root.");
|
||||
if (this.network != null) throw new IllegalStateException("Set already has a network.");
|
||||
this.network = network;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the associated network.
|
||||
*
|
||||
* @return The associated network.
|
||||
*/
|
||||
WiredNetworkImpl network() {
|
||||
return Objects.requireNonNull(find().network);
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ package dan200.computercraft.impl.network.wired;
|
||||
|
||||
import dan200.computercraft.api.network.wired.WiredNetworkChange;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.core.util.PeripheralHelpers;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@ -52,7 +53,7 @@ final class WiredNetworkChangeImpl implements WiredNetworkChange {
|
||||
var oldValue = entry.getValue();
|
||||
if (newPeripherals.containsKey(oldKey)) {
|
||||
var rightValue = added.get(oldKey);
|
||||
if (oldValue.equals(rightValue)) {
|
||||
if (PeripheralHelpers.equals(oldValue, rightValue)) {
|
||||
added.remove(oldKey);
|
||||
} else {
|
||||
removed.put(oldKey, oldValue);
|
||||
|
@ -8,6 +8,7 @@ import dan200.computercraft.api.network.Packet;
|
||||
import dan200.computercraft.api.network.wired.WiredNetwork;
|
||||
import dan200.computercraft.api.network.wired.WiredNode;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.core.util.Nullability;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
@ -187,10 +188,76 @@ final class WiredNetworkImpl implements WiredNetwork {
|
||||
return true;
|
||||
}
|
||||
|
||||
var reachable = reachableNodes(neighbours.iterator().next());
|
||||
assert neighbours.size() >= 2 : "Must have more than one neighbour.";
|
||||
|
||||
/*
|
||||
Otherwise we need to find all sets of connected nodes within the graph, and split them off into their own
|
||||
networks.
|
||||
|
||||
With our current graph representation[^1], this requires a traversal of the graph, taking O(|V| + |E))
|
||||
time, which can get quite expensive for large graphs. We try to avoid this traversal where possible, by
|
||||
optimising for the case where the graph remains fully connected after removing this node, for instance,
|
||||
removing "A" here:
|
||||
|
||||
A---B B
|
||||
| | => |
|
||||
C---D C---D
|
||||
|
||||
We observe that these sorts of loops tend to be local, and so try to identify them as quickly as possible.
|
||||
To do this, we do a standard breadth-first traversal of the graph starting at the neighbours of the
|
||||
removed node, building sets of connected nodes.
|
||||
|
||||
If, at any point, all nodes visited so far are connected to each other, then we know all remaining nodes
|
||||
will also be connected. This allows us to abort our traversal of the graph, and just remove the node (much
|
||||
like we do in the single neighbour case above).
|
||||
|
||||
Otherwise, we then just create a new network for each disjoint set of connected nodes.
|
||||
|
||||
{^1]:
|
||||
There are efficient (near-logarithmic) algorithms for this (e.g. https://arxiv.org/pdf/1609.05867.pdf),
|
||||
but they are significantly more complex to implement.
|
||||
*/
|
||||
|
||||
// Create a new set of nodes for each neighbour, and add them to our queue of nodes to visit.
|
||||
List<WiredNodeImpl> queue = new ArrayList<>();
|
||||
Set<NodeSet> nodeSets = new HashSet<>(neighbours.size());
|
||||
for (var neighbour : neighbours) {
|
||||
nodeSets.add(neighbour.currentSet = new NodeSet());
|
||||
queue.add(neighbour);
|
||||
}
|
||||
|
||||
// Perform a breadth-first search of the graph, starting from the neighbours.
|
||||
graphSearch:
|
||||
for (var i = 0; i < queue.size(); i++) {
|
||||
var enqueuedNode = queue.get(i);
|
||||
for (var neighbour : enqueuedNode.neighbours) {
|
||||
var nodeSet = Nullability.assertNonNull(enqueuedNode.currentSet).find();
|
||||
|
||||
// The neighbour has no set and so has not been visited yet. Add it to the current set and enqueue
|
||||
// it to be visited.
|
||||
if (neighbour.currentSet == null) {
|
||||
nodeSet.addNode(neighbour);
|
||||
queue.add(neighbour);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, take the union of the two nodes' sets if needed. If we've only got a single node set
|
||||
// left, then we know the whole graph is network is connected (even if not all nodes have been
|
||||
// visited) and so can abort early.
|
||||
var neighbourSet = neighbour.currentSet.find();
|
||||
if (nodeSet != neighbourSet) {
|
||||
var removed = nodeSets.remove(NodeSet.merge(nodeSet, neighbourSet));
|
||||
assert removed : "Merged set should have been ";
|
||||
if (nodeSets.size() == 1) break graphSearch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a single subset, then all nodes are reachable - just clear the set and exit.
|
||||
if (nodeSets.size() == 1) {
|
||||
assert nodeSets.iterator().next().size() == queue.size();
|
||||
for (var neighbour : queue) neighbour.currentSet = null;
|
||||
|
||||
// If all nodes are reachable then exit.
|
||||
if (reachable.size() == nodes.size()) {
|
||||
// Broadcast our simple peripheral changes
|
||||
removeSingleNode(wired, wiredNetwork);
|
||||
InvariantChecker.checkNode(wired);
|
||||
@ -198,43 +265,46 @@ final class WiredNetworkImpl implements WiredNetwork {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A split may cause 2..neighbours.size() separate networks, so we
|
||||
// iterate through our neighbour list, generating child networks.
|
||||
neighbours.removeAll(reachable);
|
||||
var maximals = new ArrayList<WiredNetworkImpl>(neighbours.size() + 1);
|
||||
maximals.add(wiredNetwork);
|
||||
maximals.add(new WiredNetworkImpl(reachable));
|
||||
assert queue.size() == nodes.size() : "Expected queue to contain all nodes.";
|
||||
|
||||
while (!neighbours.isEmpty()) {
|
||||
reachable = reachableNodes(neighbours.iterator().next());
|
||||
neighbours.removeAll(reachable);
|
||||
maximals.add(new WiredNetworkImpl(reachable));
|
||||
// Otherwise we need to create our new networks.
|
||||
var networks = new ArrayList<WiredNetworkImpl>(1 + nodeSets.size());
|
||||
// Add the network we've created for the removed node.
|
||||
networks.add(wiredNetwork);
|
||||
// And then create a new network for each disjoint subset.
|
||||
for (var set : nodeSets) {
|
||||
var network = new WiredNetworkImpl(new HashSet<>(set.size()));
|
||||
set.setNetwork(network);
|
||||
networks.add(network);
|
||||
}
|
||||
|
||||
for (var network : maximals) network.lock.writeLock().lock();
|
||||
for (var network : networks) network.lock.writeLock().lock();
|
||||
|
||||
try {
|
||||
// We special case the original node: detaching all peripherals when needed.
|
||||
wired.network = wiredNetwork;
|
||||
wired.peripherals = Map.of();
|
||||
wired.neighbours.clear();
|
||||
|
||||
// Ensure every network is finalised
|
||||
for (var network : maximals) {
|
||||
for (var child : network.nodes) {
|
||||
child.network = network;
|
||||
network.peripherals.putAll(child.peripherals);
|
||||
}
|
||||
// Add all nodes to their appropriate network.
|
||||
for (var child : queue) {
|
||||
var network = Nullability.assertNonNull(child.currentSet).network();
|
||||
child.currentSet = null;
|
||||
|
||||
child.network = network;
|
||||
network.nodes.add(child);
|
||||
network.peripherals.putAll(child.peripherals);
|
||||
}
|
||||
|
||||
for (var network : maximals) InvariantChecker.checkNetwork(network);
|
||||
for (var network : networks) InvariantChecker.checkNetwork(network);
|
||||
InvariantChecker.checkNode(wired);
|
||||
|
||||
// Then broadcast network changes once all nodes are finalised
|
||||
for (var network : maximals) {
|
||||
for (var network : networks) {
|
||||
WiredNetworkChangeImpl.changeOf(peripherals, network.peripherals).broadcast(network.nodes);
|
||||
}
|
||||
} finally {
|
||||
for (var network : maximals) network.lock.writeLock().unlock();
|
||||
for (var network : networks) network.lock.writeLock().unlock();
|
||||
}
|
||||
|
||||
nodes.clear();
|
||||
@ -373,22 +443,4 @@ final class WiredNetworkImpl implements WiredNetwork {
|
||||
throw new IllegalArgumentException("Unknown implementation of IWiredNode: " + node);
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<WiredNodeImpl> reachableNodes(WiredNodeImpl start) {
|
||||
Queue<WiredNodeImpl> enqueued = new ArrayDeque<>();
|
||||
var reachable = new HashSet<WiredNodeImpl>();
|
||||
|
||||
reachable.add(start);
|
||||
enqueued.add(start);
|
||||
|
||||
WiredNodeImpl node;
|
||||
while ((node = enqueued.poll()) != null) {
|
||||
for (var neighbour : node.neighbours) {
|
||||
// Otherwise attempt to enqueue this neighbour as well.
|
||||
if (reachable.add(neighbour)) enqueued.add(neighbour);
|
||||
}
|
||||
}
|
||||
|
||||
return reachable;
|
||||
}
|
||||
}
|
||||
|
@ -27,11 +27,39 @@ public final class WiredNodeImpl implements WiredNode {
|
||||
final HashSet<WiredNodeImpl> neighbours = new HashSet<>();
|
||||
volatile WiredNetworkImpl network;
|
||||
|
||||
/**
|
||||
* A temporary field used when checking network connectivity.
|
||||
*
|
||||
* @see WiredNetworkImpl#remove(WiredNode)
|
||||
*/
|
||||
@Nullable
|
||||
NodeSet currentSet;
|
||||
|
||||
public WiredNodeImpl(WiredElement element) {
|
||||
this.element = element;
|
||||
network = new WiredNetworkImpl(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean connectTo(WiredNode node) {
|
||||
return network.connect(this, node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean disconnectFrom(WiredNode node) {
|
||||
return network == ((WiredNodeImpl) node).network && network.disconnect(this, node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove() {
|
||||
return network.remove(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePeripherals(Map<String, IPeripheral> peripherals) {
|
||||
network.updatePeripherals(this, peripherals);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void addReceiver(PacketReceiver receiver) {
|
||||
if (receivers == null) receivers = new HashSet<>();
|
||||
|
@ -18,6 +18,7 @@ import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.dedicated.DedicatedServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.packs.resources.PreparableReloadListener;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
@ -78,10 +79,19 @@ public final class CommonHooks {
|
||||
NetworkUtils.reset();
|
||||
}
|
||||
|
||||
public static void onServerChunkUnload(LevelChunk chunk) {
|
||||
if (!(chunk.getLevel() instanceof ServerLevel)) throw new IllegalArgumentException("Not a server chunk.");
|
||||
TickScheduler.onChunkUnload(chunk);
|
||||
}
|
||||
|
||||
public static void onChunkWatch(LevelChunk chunk, ServerPlayer player) {
|
||||
MonitorWatcher.onWatch(chunk, player);
|
||||
}
|
||||
|
||||
public static void onChunkTicketLevelChanged(ServerLevel level, long chunkPos, int oldLevel, int newLevel) {
|
||||
TickScheduler.onChunkTicketChanged(level, chunkPos, oldLevel, newLevel);
|
||||
}
|
||||
|
||||
public static final ResourceLocation TREASURE_DISK_LOOT = new ResourceLocation(ComputerCraftAPI.MOD_ID, "treasure_disk");
|
||||
|
||||
private static final Set<ResourceLocation> TREASURE_DISK_LOOT_TABLES = Set.of(
|
||||
|
@ -19,7 +19,6 @@ import dan200.computercraft.impl.PocketUpgrades;
|
||||
import dan200.computercraft.impl.TurtleUpgrades;
|
||||
import dan200.computercraft.shared.command.UserLevel;
|
||||
import dan200.computercraft.shared.command.arguments.ComputerArgumentType;
|
||||
import dan200.computercraft.shared.command.arguments.ComputersArgumentType;
|
||||
import dan200.computercraft.shared.command.arguments.RepeatArgumentType;
|
||||
import dan200.computercraft.shared.command.arguments.TrackingFieldArgumentType;
|
||||
import dan200.computercraft.shared.common.ClearColourRecipe;
|
||||
@ -338,8 +337,7 @@ public final class ModRegistry {
|
||||
|
||||
static {
|
||||
register("tracking_field", TrackingFieldArgumentType.class, TrackingFieldArgumentType.metric());
|
||||
register("computer", ComputerArgumentType.class, ComputerArgumentType.oneComputer());
|
||||
register("computers", ComputersArgumentType.class, new ComputersArgumentType.Info());
|
||||
register("computer", ComputerArgumentType.class, ComputerArgumentType.get());
|
||||
registerUnsafe("repeat", RepeatArgumentType.class, new RepeatArgumentType.Info());
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,8 @@ import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import dan200.computercraft.core.computer.ComputerSide;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.command.arguments.ComputersArgumentType;
|
||||
import dan200.computercraft.shared.command.arguments.ComputerArgumentType;
|
||||
import dan200.computercraft.shared.command.arguments.ComputerSelector;
|
||||
import dan200.computercraft.shared.command.text.TableBuilder;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
@ -23,6 +24,7 @@ import dan200.computercraft.shared.computer.metrics.basic.AggregatedMetric;
|
||||
import dan200.computercraft.shared.computer.metrics.basic.BasicComputerMetricsObserver;
|
||||
import dan200.computercraft.shared.computer.metrics.basic.ComputerMetrics;
|
||||
import dan200.computercraft.shared.network.container.ComputerContainerData;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
@ -42,9 +44,6 @@ import java.util.*;
|
||||
import static dan200.computercraft.shared.command.CommandUtils.isPlayer;
|
||||
import static dan200.computercraft.shared.command.Exceptions.NOT_TRACKING_EXCEPTION;
|
||||
import static dan200.computercraft.shared.command.Exceptions.NO_TIMINGS_EXCEPTION;
|
||||
import static dan200.computercraft.shared.command.arguments.ComputerArgumentType.getComputerArgument;
|
||||
import static dan200.computercraft.shared.command.arguments.ComputerArgumentType.oneComputer;
|
||||
import static dan200.computercraft.shared.command.arguments.ComputersArgumentType.*;
|
||||
import static dan200.computercraft.shared.command.arguments.TrackingFieldArgumentType.metric;
|
||||
import static dan200.computercraft.shared.command.builder.CommandBuilder.args;
|
||||
import static dan200.computercraft.shared.command.builder.CommandBuilder.command;
|
||||
@ -70,37 +69,37 @@ public final class CommandComputerCraft {
|
||||
.requires(ModRegistry.Permissions.PERMISSION_DUMP)
|
||||
.executes(c -> dump(c.getSource()))
|
||||
.then(args()
|
||||
.arg("computer", oneComputer())
|
||||
.executes(c -> dumpComputer(c.getSource(), getComputerArgument(c, "computer")))))
|
||||
.arg("computer", ComputerArgumentType.get())
|
||||
.executes(c -> dumpComputer(c.getSource(), ComputerArgumentType.getOne(c, "computer")))))
|
||||
|
||||
.then(command("shutdown")
|
||||
.requires(ModRegistry.Permissions.PERMISSION_SHUTDOWN)
|
||||
.argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers())
|
||||
.argManyValue("computers", ComputerArgumentType.get(), ComputerSelector.all())
|
||||
.executes((c, a) -> shutdown(c.getSource(), unwrap(c.getSource(), a))))
|
||||
|
||||
.then(command("turn-on")
|
||||
.requires(ModRegistry.Permissions.PERMISSION_TURN_ON)
|
||||
.argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers())
|
||||
.argManyValue("computers", ComputerArgumentType.get(), ComputerSelector.all())
|
||||
.executes((c, a) -> turnOn(c.getSource(), unwrap(c.getSource(), a))))
|
||||
|
||||
.then(command("tp")
|
||||
.requires(ModRegistry.Permissions.PERMISSION_TP)
|
||||
.arg("computer", oneComputer())
|
||||
.executes(c -> teleport(c.getSource(), getComputerArgument(c, "computer"))))
|
||||
.arg("computer", ComputerArgumentType.get())
|
||||
.executes(c -> teleport(c.getSource(), ComputerArgumentType.getOne(c, "computer"))))
|
||||
|
||||
.then(command("queue")
|
||||
.requires(ModRegistry.Permissions.PERMISSION_QUEUE)
|
||||
.arg(
|
||||
RequiredArgumentBuilder.<CommandSourceStack, ComputersArgumentType.ComputersSupplier>argument("computer", manyComputers())
|
||||
RequiredArgumentBuilder.<CommandSourceStack, ComputerSelector>argument("computer", ComputerArgumentType.get())
|
||||
.suggests((context, builder) -> Suggestions.empty())
|
||||
)
|
||||
.argManyValue("args", StringArgumentType.string(), List.of())
|
||||
.executes((c, a) -> queue(getComputersArgument(c, "computer"), a)))
|
||||
.executes((c, a) -> queue(ComputerArgumentType.getMany(c, "computer"), a)))
|
||||
|
||||
.then(command("view")
|
||||
.requires(ModRegistry.Permissions.PERMISSION_VIEW)
|
||||
.arg("computer", oneComputer())
|
||||
.executes(c -> view(c.getSource(), getComputerArgument(c, "computer"))))
|
||||
.arg("computer", ComputerArgumentType.get())
|
||||
.executes(c -> view(c.getSource(), ComputerArgumentType.getOne(c, "computer"))))
|
||||
|
||||
.then(choice("track")
|
||||
.requires(ModRegistry.Permissions.PERMISSION_TRACK)
|
||||
@ -135,7 +134,7 @@ public final class CommandComputerCraft {
|
||||
} else if (b.getLevel() == world) {
|
||||
return 1;
|
||||
} else {
|
||||
return Integer.compare(a.getInstanceID(), b.getInstanceID());
|
||||
return a.getInstanceUUID().compareTo(b.getInstanceUUID());
|
||||
}
|
||||
});
|
||||
|
||||
@ -160,7 +159,8 @@ public final class CommandComputerCraft {
|
||||
*/
|
||||
private static int dumpComputer(CommandSourceStack source, ServerComputer computer) {
|
||||
var table = new TableBuilder("Dump");
|
||||
table.row(header("Instance"), text(Integer.toString(computer.getInstanceID())));
|
||||
table.row(header("Instance ID"), text(Integer.toString(computer.getInstanceID())));
|
||||
table.row(header("Instance UUID"), text(computer.getInstanceUUID().toString()));
|
||||
table.row(header("Id"), text(Integer.toString(computer.getID())));
|
||||
table.row(header("Label"), text(computer.getLabel()));
|
||||
table.row(header("On"), bool(computer.isOn()));
|
||||
@ -332,29 +332,22 @@ public final class CommandComputerCraft {
|
||||
|
||||
// Additional helper functions.
|
||||
|
||||
private static Component linkComputer(CommandSourceStack source, @Nullable ServerComputer serverComputer, int computerId) {
|
||||
private static Component linkComputer(CommandSourceStack source, @Nullable ServerComputer computer, int computerId) {
|
||||
var out = Component.literal("");
|
||||
|
||||
// Append the computer instance
|
||||
if (serverComputer == null) {
|
||||
out.append(text("?"));
|
||||
// And instance
|
||||
if (computer == null) {
|
||||
out.append("#" + computerId + " ").append(coloured("(unloaded)", ChatFormatting.GRAY));
|
||||
} else {
|
||||
out.append(link(
|
||||
text(Integer.toString(serverComputer.getInstanceID())),
|
||||
"/computercraft dump " + serverComputer.getInstanceID(),
|
||||
Component.translatable("commands.computercraft.dump.action")
|
||||
));
|
||||
out.append(makeComputerDumpCommand(computer));
|
||||
}
|
||||
|
||||
// And ID
|
||||
out.append(" (id " + computerId + ")");
|
||||
|
||||
// And, if we're a player, some useful links
|
||||
if (serverComputer != null && isPlayer(source)) {
|
||||
if (computer != null && isPlayer(source)) {
|
||||
if (ModRegistry.Permissions.PERMISSION_TP.test(source)) {
|
||||
out.append(" ").append(link(
|
||||
text("\u261b"),
|
||||
"/computercraft tp " + serverComputer.getInstanceID(),
|
||||
makeComputerCommand("tp", computer),
|
||||
Component.translatable("commands.computercraft.tp.action")
|
||||
));
|
||||
}
|
||||
@ -362,7 +355,7 @@ public final class CommandComputerCraft {
|
||||
if (ModRegistry.Permissions.PERMISSION_VIEW.test(source)) {
|
||||
out.append(" ").append(link(
|
||||
text("\u20e2"),
|
||||
"/computercraft view " + serverComputer.getInstanceID(),
|
||||
makeComputerCommand("view", computer),
|
||||
Component.translatable("commands.computercraft.view.action")
|
||||
));
|
||||
}
|
||||
@ -380,7 +373,7 @@ public final class CommandComputerCraft {
|
||||
if (ModRegistry.Permissions.PERMISSION_TP.test(context)) {
|
||||
return link(
|
||||
position(computer.getPosition()),
|
||||
"/computercraft tp " + computer.getInstanceID(),
|
||||
makeComputerCommand("tp", computer),
|
||||
Component.translatable("commands.computercraft.tp.action")
|
||||
);
|
||||
} else {
|
||||
@ -392,7 +385,7 @@ public final class CommandComputerCraft {
|
||||
var file = new File(ServerContext.get(source.getServer()).storageDir().toFile(), "computer/" + id);
|
||||
if (!file.isDirectory()) return null;
|
||||
|
||||
return link(
|
||||
return clientLink(
|
||||
text("\u270E"),
|
||||
"/" + CLIENT_OPEN_FOLDER + " " + id,
|
||||
Component.translatable("commands.computercraft.dump.open_path")
|
||||
@ -431,4 +424,10 @@ public final class CommandComputerCraft {
|
||||
table.display(source);
|
||||
return timings.size();
|
||||
}
|
||||
|
||||
public static Set<ServerComputer> unwrap(CommandSourceStack source, Collection<ComputerSelector> suppliers) {
|
||||
Set<ServerComputer> computers = new HashSet<>();
|
||||
for (var supplier : suppliers) supplier.find(source).forEach(computers::add);
|
||||
return computers;
|
||||
}
|
||||
}
|
||||
|
@ -28,12 +28,12 @@ public final class CommandUtils {
|
||||
@SuppressWarnings("unchecked")
|
||||
public static CompletableFuture<Suggestions> suggestOnServer(CommandContext<?> context, Function<CommandContext<CommandSourceStack>, CompletableFuture<Suggestions>> supplier) {
|
||||
var source = context.getSource();
|
||||
if (!(source instanceof SharedSuggestionProvider)) {
|
||||
if (!(source instanceof SharedSuggestionProvider shared)) {
|
||||
return Suggestions.empty();
|
||||
} else if (source instanceof CommandSourceStack) {
|
||||
return supplier.apply((CommandContext<CommandSourceStack>) context);
|
||||
} else {
|
||||
return ((SharedSuggestionProvider) source).customSuggestion(context);
|
||||
return shared.customSuggestion(context);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,8 @@ package dan200.computercraft.shared.command;
|
||||
import com.mojang.brigadier.exceptions.Dynamic2CommandExceptionType;
|
||||
import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;
|
||||
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
|
||||
import net.minecraft.commands.arguments.selector.EntitySelectorParser;
|
||||
import net.minecraft.commands.arguments.selector.options.EntitySelectorOptions;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
public final class Exceptions {
|
||||
@ -20,6 +22,13 @@ public final class Exceptions {
|
||||
|
||||
public static final SimpleCommandExceptionType ARGUMENT_EXPECTED = translated("argument.computercraft.argument_expected");
|
||||
|
||||
public static final DynamicCommandExceptionType UNKNOWN_FAMILY = translated1("argument.computercraft.unknown_computer_family");
|
||||
|
||||
public static final DynamicCommandExceptionType ERROR_EXPECTED_OPTION_VALUE = EntitySelectorParser.ERROR_EXPECTED_OPTION_VALUE;
|
||||
public static final SimpleCommandExceptionType ERROR_EXPECTED_END_OF_OPTIONS = EntitySelectorParser.ERROR_EXPECTED_END_OF_OPTIONS;
|
||||
public static final DynamicCommandExceptionType ERROR_UNKNOWN_OPTION = EntitySelectorOptions.ERROR_UNKNOWN_OPTION;
|
||||
public static final DynamicCommandExceptionType ERROR_INAPPLICABLE_OPTION = EntitySelectorOptions.ERROR_INAPPLICABLE_OPTION;
|
||||
|
||||
private static SimpleCommandExceptionType translated(String key) {
|
||||
return new SimpleCommandExceptionType(Component.translatable(key));
|
||||
}
|
||||
|
@ -0,0 +1,29 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.command.arguments;
|
||||
|
||||
import com.mojang.brigadier.StringReader;
|
||||
|
||||
final class ArgumentParserUtils {
|
||||
private ArgumentParserUtils() {
|
||||
}
|
||||
|
||||
public static boolean consume(StringReader reader, char lookahead) {
|
||||
if (!reader.canRead() || reader.peek() != lookahead) return false;
|
||||
|
||||
reader.skip();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean consume(StringReader reader, String lookahead) {
|
||||
if (!reader.canRead(lookahead.length())) return false;
|
||||
for (var i = 0; i < lookahead.length(); i++) {
|
||||
if (reader.peek(i) != lookahead.charAt(i)) return false;
|
||||
}
|
||||
|
||||
reader.setCursor(reader.getCursor() + lookahead.length());
|
||||
return true;
|
||||
}
|
||||
}
|
@ -14,66 +14,58 @@ import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static dan200.computercraft.shared.command.Exceptions.COMPUTER_ARG_MANY;
|
||||
|
||||
public final class ComputerArgumentType implements ArgumentType<ComputerArgumentType.ComputerSupplier> {
|
||||
public final class ComputerArgumentType implements ArgumentType<ComputerSelector> {
|
||||
private static final ComputerArgumentType INSTANCE = new ComputerArgumentType();
|
||||
|
||||
public static ComputerArgumentType oneComputer() {
|
||||
return INSTANCE;
|
||||
}
|
||||
private static final List<String> EXAMPLES = List.of(
|
||||
"0", "123", "@c[instance_id=123]"
|
||||
);
|
||||
|
||||
public static ServerComputer getComputerArgument(CommandContext<CommandSourceStack> context, String name) throws CommandSyntaxException {
|
||||
return context.getArgument(name, ComputerSupplier.class).unwrap(context.getSource());
|
||||
public static ComputerArgumentType get() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private ComputerArgumentType() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a list of computers from a {@link CommandContext} argument.
|
||||
*
|
||||
* @param context The current command context.
|
||||
* @param name The name of the argument.
|
||||
* @return The found computer(s).
|
||||
*/
|
||||
public static List<ServerComputer> getMany(CommandContext<CommandSourceStack> context, String name) {
|
||||
return context.getArgument(name, ComputerSelector.class).find(context.getSource()).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a single computer from a {@link CommandContext} argument.
|
||||
*
|
||||
* @param context The current command context.
|
||||
* @param name The name of the argument.
|
||||
* @return The found computer.
|
||||
* @throws CommandSyntaxException If exactly one computer could not be found.
|
||||
*/
|
||||
public static ServerComputer getOne(CommandContext<CommandSourceStack> context, String name) throws CommandSyntaxException {
|
||||
return context.getArgument(name, ComputerSelector.class).findOne(context.getSource());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComputerSupplier parse(StringReader reader) throws CommandSyntaxException {
|
||||
var start = reader.getCursor();
|
||||
var supplier = ComputersArgumentType.someComputers().parse(reader);
|
||||
var selector = reader.getString().substring(start, reader.getCursor());
|
||||
|
||||
return s -> {
|
||||
var computers = supplier.unwrap(s);
|
||||
|
||||
if (computers.size() == 1) return computers.iterator().next();
|
||||
|
||||
var builder = new StringBuilder();
|
||||
var first = true;
|
||||
for (var computer : computers) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
builder.append(", ");
|
||||
}
|
||||
|
||||
builder.append(computer.getInstanceID());
|
||||
}
|
||||
|
||||
|
||||
// We have an incorrect number of computers: reset and throw an error
|
||||
reader.setCursor(start);
|
||||
throw COMPUTER_ARG_MANY.createWithContext(reader, selector, builder.toString());
|
||||
};
|
||||
public ComputerSelector parse(StringReader reader) throws CommandSyntaxException {
|
||||
return ComputerSelector.parse(reader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
|
||||
return ComputersArgumentType.someComputers().listSuggestions(context, builder);
|
||||
return ComputerSelector.suggest(context, builder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getExamples() {
|
||||
return ComputersArgumentType.someComputers().getExamples();
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ComputerSupplier {
|
||||
ServerComputer unwrap(CommandSourceStack source) throws CommandSyntaxException;
|
||||
return EXAMPLES;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,387 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.command.arguments;
|
||||
|
||||
import com.mojang.brigadier.Message;
|
||||
import com.mojang.brigadier.StringReader;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||
import net.minecraft.advancements.critereon.MinMaxBounds;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.SharedSuggestionProvider;
|
||||
import net.minecraft.commands.arguments.UuidArgument;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static dan200.computercraft.shared.command.CommandUtils.suggestOnServer;
|
||||
import static dan200.computercraft.shared.command.Exceptions.*;
|
||||
import static dan200.computercraft.shared.command.arguments.ArgumentParserUtils.consume;
|
||||
import static dan200.computercraft.shared.command.text.ChatHelpers.makeComputerDumpCommand;
|
||||
|
||||
public record ComputerSelector(
|
||||
String selector,
|
||||
OptionalInt instanceId,
|
||||
@Nullable UUID instanceUuid,
|
||||
OptionalInt computerId,
|
||||
@Nullable String label,
|
||||
@Nullable ComputerFamily family,
|
||||
@Nullable AABB bounds,
|
||||
@Nullable MinMaxBounds.Doubles range
|
||||
) {
|
||||
private static final ComputerSelector all = new ComputerSelector("@c[]", OptionalInt.empty(), null, OptionalInt.empty(), null, null, null, null);
|
||||
|
||||
private static UuidArgument uuidArgument = UuidArgument.uuid();
|
||||
|
||||
/**
|
||||
* A {@link ComputerSelector} which matches all computers.
|
||||
*
|
||||
* @return A {@link ComputerSelector} instance.
|
||||
*/
|
||||
public static ComputerSelector all() {
|
||||
return all;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all computers matching this selector.
|
||||
*
|
||||
* @param source The source requesting these computers.
|
||||
* @return The stream of matching computers.
|
||||
*/
|
||||
public Stream<ServerComputer> find(CommandSourceStack source) {
|
||||
var context = ServerContext.get(source.getServer());
|
||||
if (instanceId().isPresent()) {
|
||||
var computer = context.registry().get(instanceId().getAsInt());
|
||||
return computer != null && matches(source, computer) ? Stream.of(computer) : Stream.of();
|
||||
}
|
||||
|
||||
if (instanceUuid() != null) {
|
||||
var computer = context.registry().get(instanceUuid());
|
||||
return computer != null && matches(source, computer) ? Stream.of(computer) : Stream.of();
|
||||
}
|
||||
|
||||
return context.registry().getComputers().stream().filter(c -> matches(source, c));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find exactly one computer which matches this selector.
|
||||
*
|
||||
* @param source The source requesting this computer.
|
||||
* @return The computer.
|
||||
* @throws CommandSyntaxException If no or multiple computers could be found.
|
||||
*/
|
||||
public ServerComputer findOne(CommandSourceStack source) throws CommandSyntaxException {
|
||||
var computers = find(source).toList();
|
||||
if (computers.isEmpty()) throw COMPUTER_ARG_NONE.create(selector);
|
||||
if (computers.size() == 1) return computers.iterator().next();
|
||||
|
||||
var builder = Component.empty();
|
||||
var first = true;
|
||||
for (var computer : computers) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
builder.append(", ");
|
||||
}
|
||||
|
||||
builder.append(makeComputerDumpCommand(computer));
|
||||
}
|
||||
|
||||
|
||||
// We have an incorrect number of computers: throw an error
|
||||
throw COMPUTER_ARG_MANY.create(selector, builder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this selector matches a given computer.
|
||||
*
|
||||
* @param source The command source, used for distance comparisons.
|
||||
* @param computer The computer to check.
|
||||
* @return If this computer is matched by the selector.
|
||||
*/
|
||||
public boolean matches(CommandSourceStack source, ServerComputer computer) {
|
||||
return (instanceId().isEmpty() || computer.getInstanceID() == instanceId().getAsInt())
|
||||
&& (instanceUuid() == null || computer.getInstanceUUID().equals(instanceUuid()))
|
||||
&& (computerId().isEmpty() || computer.getID() == computerId().getAsInt())
|
||||
&& (label == null || Objects.equals(computer.getLabel(), label))
|
||||
&& (family == null || computer.getFamily() == family)
|
||||
&& (bounds == null || (source.getLevel() == computer.getLevel() && bounds.contains(Vec3.atCenterOf(computer.getPosition()))))
|
||||
&& (range == null || (source.getLevel() == computer.getLevel() && range.matchesSqr(source.getPosition().distanceToSqr(Vec3.atCenterOf(computer.getPosition())))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an input string.
|
||||
*
|
||||
* @param reader The reader to parse from.
|
||||
* @return The parsed selector.
|
||||
* @throws CommandSyntaxException If the selector was incomplete or malformed.
|
||||
*/
|
||||
public static ComputerSelector parse(StringReader reader) throws CommandSyntaxException {
|
||||
var start = reader.getCursor();
|
||||
|
||||
var builder = new Builder();
|
||||
|
||||
if (consume(reader, "@c[")) {
|
||||
parseSelector(builder, reader);
|
||||
} else {
|
||||
// TODO(1.20.5): Only parse computer ids here.
|
||||
var kind = reader.peek();
|
||||
if (kind == '@') {
|
||||
reader.skip();
|
||||
builder.label = reader.readString();
|
||||
} else if (kind == '~') {
|
||||
reader.skip();
|
||||
builder.family = parseFamily(reader);
|
||||
} else if (kind == '#') {
|
||||
reader.skip();
|
||||
builder.computerId = OptionalInt.of(reader.readInt());
|
||||
} else {
|
||||
builder.instanceId = OptionalInt.of(reader.readInt());
|
||||
}
|
||||
}
|
||||
|
||||
var selector = reader.getString().substring(start, reader.getCursor());
|
||||
return new ComputerSelector(selector, builder.instanceId, builder.instanceUuid, builder.computerId, builder.label, builder.family, builder.bounds, builder.range);
|
||||
}
|
||||
|
||||
private static void parseSelector(Builder builder, StringReader reader) throws CommandSyntaxException {
|
||||
Set<Option> seenOptions = new HashSet<>();
|
||||
while (true) {
|
||||
reader.skipWhitespace();
|
||||
|
||||
if (!reader.canRead()) throw ERROR_EXPECTED_END_OF_OPTIONS.createWithContext(reader);
|
||||
if (consume(reader, ']')) break;
|
||||
|
||||
// Read the option and validate it.
|
||||
var option = parseOption(reader, seenOptions);
|
||||
reader.skipWhitespace();
|
||||
if (!consume(reader, '=')) throw ERROR_EXPECTED_OPTION_VALUE.createWithContext(reader, option.name());
|
||||
reader.skipWhitespace();
|
||||
option.parser.parse(reader, builder);
|
||||
reader.skipWhitespace();
|
||||
|
||||
if (consume(reader, ']')) break;
|
||||
if (!consume(reader, ',')) throw ERROR_EXPECTED_END_OF_OPTIONS.createWithContext(reader);
|
||||
}
|
||||
}
|
||||
|
||||
private static Option parseOption(StringReader reader, Set<Option> seen) throws CommandSyntaxException {
|
||||
var start = reader.getCursor();
|
||||
var name = reader.readUnquotedString();
|
||||
var option = options.get(name);
|
||||
if (option == null) {
|
||||
reader.setCursor(start);
|
||||
throw ERROR_UNKNOWN_OPTION.createWithContext(reader, name);
|
||||
} else if (!seen.add(option)) {
|
||||
throw ERROR_INAPPLICABLE_OPTION.createWithContext(reader, name);
|
||||
}
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
private static ComputerFamily parseFamily(StringReader reader) throws CommandSyntaxException {
|
||||
var start = reader.getCursor();
|
||||
var name = reader.readUnquotedString();
|
||||
var family = Arrays.stream(ComputerFamily.values()).filter(x -> x.name().equalsIgnoreCase(name)).findFirst().orElse(null);
|
||||
if (family == null) {
|
||||
reader.setCursor(start);
|
||||
throw UNKNOWN_FAMILY.createWithContext(reader, name);
|
||||
}
|
||||
|
||||
return family;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest completions for a selector argument.
|
||||
*
|
||||
* @param context The current command context.
|
||||
* @param builder The builder containing the current input.
|
||||
* @return The possible suggestions.
|
||||
*/
|
||||
public static CompletableFuture<Suggestions> suggest(CommandContext<?> context, SuggestionsBuilder builder) {
|
||||
var remaining = builder.getRemaining();
|
||||
|
||||
if (remaining.startsWith("@")) {
|
||||
var reader = new StringReader(builder.getInput());
|
||||
reader.setCursor(builder.getStart());
|
||||
return suggestSelector(context, reader);
|
||||
} else if (remaining.startsWith("#")) {
|
||||
return suggestComputers(c -> "#" + c.getID()).suggest(context, builder);
|
||||
} else {
|
||||
return suggestComputers(c -> Integer.toString(c.getInstanceID())).suggest(context, builder);
|
||||
}
|
||||
}
|
||||
|
||||
private static CompletableFuture<Suggestions> suggestSelector(CommandContext<?> context, StringReader reader) {
|
||||
Set<Option> seenOptions = new HashSet<>();
|
||||
var builder = new Builder();
|
||||
|
||||
if (!consume(reader, "@c[")) return suggestions(reader).suggest("@c[").buildFuture();
|
||||
|
||||
while (true) {
|
||||
reader.skipWhitespace();
|
||||
|
||||
if (!reader.canRead()) return suggestOptions(reader);
|
||||
if (consume(reader, ']')) break;
|
||||
|
||||
// Read the option and validate it.
|
||||
Option option;
|
||||
try {
|
||||
option = parseOption(reader, seenOptions);
|
||||
} catch (CommandSyntaxException e) {
|
||||
return suggestOptions(reader);
|
||||
}
|
||||
reader.skipWhitespace();
|
||||
if (!consume(reader, '=')) return suggestions(reader).suggest("=").buildFuture();
|
||||
reader.skipWhitespace();
|
||||
try {
|
||||
option.parser.parse(reader, builder);
|
||||
} catch (CommandSyntaxException e) {
|
||||
return option.suggest.suggest(context, suggestions(reader));
|
||||
}
|
||||
reader.skipWhitespace();
|
||||
|
||||
if (consume(reader, ']')) break;
|
||||
if (!consume(reader, ',')) return suggestions(reader).suggest(",").buildFuture();
|
||||
}
|
||||
|
||||
return Suggestions.empty();
|
||||
}
|
||||
|
||||
private static CompletableFuture<Suggestions> suggestOptions(StringReader reader) {
|
||||
return SharedSuggestionProvider.suggest(options().values(), suggestions(reader), Option::name, Option::tooltip);
|
||||
}
|
||||
|
||||
private static SuggestionsBuilder suggestions(StringReader reader) {
|
||||
return new SuggestionsBuilder(reader.getString(), reader.getCursor());
|
||||
}
|
||||
|
||||
private static final class Builder {
|
||||
private OptionalInt instanceId = OptionalInt.empty();
|
||||
private @Nullable UUID instanceUuid = null;
|
||||
private OptionalInt computerId = OptionalInt.empty();
|
||||
private @Nullable String label;
|
||||
private @Nullable ComputerFamily family;
|
||||
private @Nullable AABB bounds;
|
||||
private @Nullable MinMaxBounds.Doubles range;
|
||||
}
|
||||
|
||||
private static final Map<String, Option> options;
|
||||
|
||||
/**
|
||||
* Get a map of individual selector options.
|
||||
*
|
||||
* @return The available options.
|
||||
*/
|
||||
public static Map<String, Option> options() {
|
||||
return options;
|
||||
}
|
||||
|
||||
static {
|
||||
var optionList = new Option[]{
|
||||
new Option(
|
||||
"instance",
|
||||
(reader, builder) -> builder.instanceUuid = uuidArgument.parse(reader),
|
||||
suggestComputers(c -> c.getInstanceUUID().toString())
|
||||
),
|
||||
new Option(
|
||||
"id",
|
||||
(reader, builder) -> builder.computerId = OptionalInt.of(reader.readInt()),
|
||||
suggestComputers(c -> Integer.toString(c.getID()))
|
||||
),
|
||||
new Option(
|
||||
"label",
|
||||
(reader, builder) -> builder.label = reader.readQuotedString(),
|
||||
suggestComputers(ServerComputer::getLabel)
|
||||
),
|
||||
new Option(
|
||||
"family",
|
||||
(reader, builder) -> builder.family = parseFamily(reader),
|
||||
(source, builder) -> SharedSuggestionProvider.suggest(Arrays.stream(ComputerFamily.values()).map(x -> x.name().toLowerCase(Locale.ROOT)), builder)
|
||||
),
|
||||
new Option(
|
||||
"distance",
|
||||
(reader, builder) -> builder.range = MinMaxBounds.Doubles.fromReader(reader),
|
||||
(source, builder) -> Suggestions.empty()
|
||||
),
|
||||
};
|
||||
|
||||
options = Arrays.stream(optionList).collect(Collectors.toUnmodifiableMap(Option::name, x -> x));
|
||||
}
|
||||
|
||||
/**
|
||||
* A single option to filter a computer by.
|
||||
*/
|
||||
public static final class Option {
|
||||
private final String name;
|
||||
private final Parser parser;
|
||||
private final SuggestionProvider suggest;
|
||||
private final String translationKey;
|
||||
private final Message tooltip;
|
||||
|
||||
|
||||
Option(String name, Parser parser, SuggestionProvider suggest) {
|
||||
this.name = name;
|
||||
this.parser = parser;
|
||||
this.suggest = suggest;
|
||||
tooltip = Component.translatable(translationKey = "argument.computercraft.computer." + name);
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of this selector.
|
||||
*
|
||||
* @return The selector's name.
|
||||
*/
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Message tooltip() {
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
/**
|
||||
* The translation key for this selector.
|
||||
*
|
||||
* @return The selector's translation key.
|
||||
*/
|
||||
public String translationKey() {
|
||||
return translationKey;
|
||||
}
|
||||
}
|
||||
|
||||
private interface Parser {
|
||||
void parse(StringReader reader, Builder builder) throws CommandSyntaxException;
|
||||
}
|
||||
|
||||
private interface SuggestionProvider {
|
||||
CompletableFuture<Suggestions> suggest(CommandContext<?> source, SuggestionsBuilder builder);
|
||||
}
|
||||
|
||||
private static SuggestionProvider suggestComputers(Function<ServerComputer, String> renderer) {
|
||||
return (anyContext, builder) -> suggestOnServer(anyContext, context -> {
|
||||
var remaining = builder.getRemaining();
|
||||
for (var computer : ServerContext.get(context.getSource().getServer()).registry().getComputers()) {
|
||||
var converted = renderer.apply(computer);
|
||||
if (converted != null && converted.startsWith(remaining)) {
|
||||
builder.suggest(converted);
|
||||
}
|
||||
}
|
||||
return builder.buildFuture();
|
||||
});
|
||||
}
|
||||
}
|
@ -1,188 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.command.arguments;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.mojang.brigadier.StringReader;
|
||||
import com.mojang.brigadier.arguments.ArgumentType;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||
import net.minecraft.commands.CommandBuildContext;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static dan200.computercraft.shared.command.CommandUtils.suggest;
|
||||
import static dan200.computercraft.shared.command.CommandUtils.suggestOnServer;
|
||||
import static dan200.computercraft.shared.command.Exceptions.COMPUTER_ARG_NONE;
|
||||
|
||||
public final class ComputersArgumentType implements ArgumentType<ComputersArgumentType.ComputersSupplier> {
|
||||
private static final ComputersArgumentType MANY = new ComputersArgumentType(false);
|
||||
private static final ComputersArgumentType SOME = new ComputersArgumentType(true);
|
||||
|
||||
private static final List<String> EXAMPLES = List.of(
|
||||
"0", "#0", "@Label", "~Advanced"
|
||||
);
|
||||
|
||||
public static ComputersArgumentType manyComputers() {
|
||||
return MANY;
|
||||
}
|
||||
|
||||
public static ComputersArgumentType someComputers() {
|
||||
return SOME;
|
||||
}
|
||||
|
||||
public static Collection<ServerComputer> getComputersArgument(CommandContext<CommandSourceStack> context, String name) throws CommandSyntaxException {
|
||||
return context.getArgument(name, ComputersSupplier.class).unwrap(context.getSource());
|
||||
}
|
||||
|
||||
private final boolean requireSome;
|
||||
|
||||
private ComputersArgumentType(boolean requireSome) {
|
||||
this.requireSome = requireSome;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComputersSupplier parse(StringReader reader) throws CommandSyntaxException {
|
||||
var start = reader.getCursor();
|
||||
var kind = reader.peek();
|
||||
ComputersSupplier computers;
|
||||
if (kind == '@') {
|
||||
reader.skip();
|
||||
var label = reader.readUnquotedString();
|
||||
computers = getComputers(x -> Objects.equals(label, x.getLabel()));
|
||||
} else if (kind == '~') {
|
||||
reader.skip();
|
||||
var family = reader.readUnquotedString();
|
||||
computers = getComputers(x -> x.getFamily().name().equalsIgnoreCase(family));
|
||||
} else if (kind == '#') {
|
||||
reader.skip();
|
||||
var id = reader.readInt();
|
||||
computers = getComputers(x -> x.getID() == id);
|
||||
} else {
|
||||
var instance = reader.readInt();
|
||||
computers = s -> {
|
||||
var computer = ServerContext.get(s.getServer()).registry().get(instance);
|
||||
return computer == null ? List.of() : List.of(computer);
|
||||
};
|
||||
}
|
||||
|
||||
if (requireSome) {
|
||||
var selector = reader.getString().substring(start, reader.getCursor());
|
||||
return source -> {
|
||||
var matched = computers.unwrap(source);
|
||||
if (matched.isEmpty()) throw COMPUTER_ARG_NONE.create(selector);
|
||||
return matched;
|
||||
};
|
||||
} else {
|
||||
return computers;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
|
||||
var remaining = builder.getRemaining();
|
||||
|
||||
// We can run this one on the client, for obvious reasons.
|
||||
if (remaining.startsWith("~")) {
|
||||
return suggest(builder, ComputerFamily.values(), x -> "~" + x.name());
|
||||
}
|
||||
|
||||
// Verify we've a command source and we're running on the server
|
||||
return suggestOnServer(context, s -> {
|
||||
if (remaining.startsWith("@")) {
|
||||
suggestComputers(s.getSource(), builder, remaining, x -> {
|
||||
var label = x.getLabel();
|
||||
return label == null ? null : "@" + label;
|
||||
});
|
||||
} else if (remaining.startsWith("#")) {
|
||||
suggestComputers(s.getSource(), builder, remaining, c -> "#" + c.getID());
|
||||
} else {
|
||||
suggestComputers(s.getSource(), builder, remaining, c -> Integer.toString(c.getInstanceID()));
|
||||
}
|
||||
|
||||
return builder.buildFuture();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getExamples() {
|
||||
return EXAMPLES;
|
||||
}
|
||||
|
||||
private static void suggestComputers(CommandSourceStack source, SuggestionsBuilder builder, String remaining, Function<ServerComputer, String> renderer) {
|
||||
remaining = remaining.toLowerCase(Locale.ROOT);
|
||||
for (var computer : ServerContext.get(source.getServer()).registry().getComputers()) {
|
||||
var converted = renderer.apply(computer);
|
||||
if (converted != null && converted.toLowerCase(Locale.ROOT).startsWith(remaining)) {
|
||||
builder.suggest(converted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ComputersSupplier getComputers(Predicate<ServerComputer> predicate) {
|
||||
return s -> ServerContext.get(s.getServer()).registry()
|
||||
.getComputers()
|
||||
.stream()
|
||||
.filter(predicate)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public static class Info implements ArgumentTypeInfo<ComputersArgumentType, Template> {
|
||||
@Override
|
||||
public void serializeToNetwork(ComputersArgumentType.Template arg, FriendlyByteBuf buf) {
|
||||
buf.writeBoolean(arg.requireSome());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComputersArgumentType.Template deserializeFromNetwork(FriendlyByteBuf buf) {
|
||||
var requiresSome = buf.readBoolean();
|
||||
return new ComputersArgumentType.Template(this, requiresSome);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serializeToJson(ComputersArgumentType.Template arg, JsonObject json) {
|
||||
json.addProperty("requireSome", arg.requireSome);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComputersArgumentType.Template unpack(ComputersArgumentType argumentType) {
|
||||
return new ComputersArgumentType.Template(this, argumentType.requireSome);
|
||||
}
|
||||
}
|
||||
|
||||
public record Template(Info info, boolean requireSome) implements ArgumentTypeInfo.Template<ComputersArgumentType> {
|
||||
@Override
|
||||
public ComputersArgumentType instantiate(CommandBuildContext context) {
|
||||
return requireSome ? SOME : MANY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Info type() {
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ComputersSupplier {
|
||||
Collection<ServerComputer> unwrap(CommandSourceStack source) throws CommandSyntaxException;
|
||||
}
|
||||
|
||||
public static Set<ServerComputer> unwrap(CommandSourceStack source, Collection<ComputersSupplier> suppliers) throws CommandSyntaxException {
|
||||
Set<ServerComputer> computers = new HashSet<>();
|
||||
for (var supplier : suppliers) computers.addAll(supplier.unwrap(source));
|
||||
return computers;
|
||||
}
|
||||
}
|
@ -4,6 +4,8 @@
|
||||
|
||||
package dan200.computercraft.shared.command.text;
|
||||
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.platform.PlatformHelper;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.ClickEvent;
|
||||
@ -53,6 +55,13 @@ public final class ChatHelpers {
|
||||
return link(component, new ClickEvent(ClickEvent.Action.RUN_COMMAND, command), toolTip);
|
||||
}
|
||||
|
||||
public static Component clientLink(MutableComponent component, String command, Component toolTip) {
|
||||
var event = PlatformHelper.get().canClickRunClientCommand()
|
||||
? new ClickEvent(ClickEvent.Action.RUN_COMMAND, command)
|
||||
: new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, command);
|
||||
return link(component, event, toolTip);
|
||||
}
|
||||
|
||||
public static Component link(Component component, ClickEvent click, Component toolTip) {
|
||||
var style = component.getStyle();
|
||||
|
||||
@ -73,4 +82,16 @@ public final class ChatHelpers {
|
||||
.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.translatable("gui.computercraft.tooltip.copy")))
|
||||
);
|
||||
}
|
||||
|
||||
public static String makeComputerCommand(String command, ServerComputer computer) {
|
||||
return String.format("/computercraft %s @c[instance=%s]", command, computer.getInstanceUUID());
|
||||
}
|
||||
|
||||
public static Component makeComputerDumpCommand(ServerComputer computer) {
|
||||
return link(
|
||||
text("#" + computer.getID()),
|
||||
makeComputerCommand("dump", computer),
|
||||
Component.translatable("commands.computercraft.dump.action")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.BlockGetter;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelAccessor;
|
||||
import net.minecraft.world.level.LevelReader;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.EntityBlock;
|
||||
@ -176,6 +177,15 @@ public abstract class AbstractComputerBlock<T extends AbstractComputerBlockEntit
|
||||
if (be instanceof AbstractComputerBlockEntity computer) computer.neighborChanged(neighbour);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor level, BlockPos pos, BlockPos neighborPos) {
|
||||
var be = level.getBlockEntity(pos);
|
||||
if (be instanceof AbstractComputerBlockEntity computer) computer.neighbourShapeChanged(direction);
|
||||
|
||||
return super.updateShape(state, direction, neighborState, level, pos, neighborPos);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
@Deprecated
|
||||
|
@ -36,13 +36,14 @@ import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class AbstractComputerBlockEntity extends BlockEntity implements IComputerBlockEntity, Nameable, MenuProvider {
|
||||
private static final String NBT_ID = "ComputerId";
|
||||
private static final String NBT_LABEL = "Label";
|
||||
private static final String NBT_ON = "On";
|
||||
|
||||
private int instanceID = -1;
|
||||
private @Nullable UUID instanceID = null;
|
||||
private int computerID = -1;
|
||||
protected @Nullable String label = null;
|
||||
private boolean on = false;
|
||||
@ -66,7 +67,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
|
||||
|
||||
var computer = getServerComputer();
|
||||
if (computer != null) computer.close();
|
||||
instanceID = -1;
|
||||
instanceID = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -113,19 +114,16 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
public void neighborChanged(BlockPos neighbour) {
|
||||
updateInputAt(neighbour);
|
||||
}
|
||||
|
||||
protected void serverTick() {
|
||||
if (getLevel().isClientSide) return;
|
||||
if (computerID < 0 && !startOn) return; // Don't tick if we don't need a computer!
|
||||
|
||||
var computer = createServerComputer();
|
||||
|
||||
// Update any peripherals that have changed.
|
||||
if (invalidSides != 0) {
|
||||
for (var direction : DirectionUtil.FACINGS) {
|
||||
if ((invalidSides & (1 << direction.ordinal())) != 0) refreshPeripheral(computer, direction);
|
||||
if (DirectionUtil.isSet(invalidSides, direction)) refreshPeripheral(computer, direction);
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,16 +137,30 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
|
||||
|
||||
fresh = false;
|
||||
computerID = computer.getID();
|
||||
label = computer.getLabel();
|
||||
on = computer.isOn();
|
||||
|
||||
// Update the block state if needed. We don't fire a block update intentionally,
|
||||
// as this only really is needed on the client side.
|
||||
// If the on state has changed, mark as as dirty.
|
||||
var newOn = computer.isOn();
|
||||
if (on != newOn) {
|
||||
on = newOn;
|
||||
setChanged();
|
||||
}
|
||||
|
||||
// If the label has changed, mark as dirty and sync to client.
|
||||
var newLabel = computer.getLabel();
|
||||
if (!Objects.equals(label, newLabel)) {
|
||||
label = newLabel;
|
||||
BlockEntityHelpers.updateBlock(this);
|
||||
}
|
||||
|
||||
// Update the block state if needed.
|
||||
updateBlockState(computer.getState());
|
||||
|
||||
// TODO: This should ideally be split up into label/id/on (which should save NBT and sync to client) and
|
||||
// redstone (which should update outputs)
|
||||
if (computer.hasOutputChanged()) updateOutput();
|
||||
var changes = computer.pollAndResetChanges();
|
||||
if (changes != 0) {
|
||||
for (var direction : DirectionUtil.FACINGS) {
|
||||
if ((changes & (1 << remapToLocalSide(direction).ordinal())) != 0) updateRedstoneTo(direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void updateBlockState(ComputerState newState);
|
||||
@ -198,11 +210,15 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
|
||||
return localSide;
|
||||
}
|
||||
|
||||
private void updateRedstoneInputs(ServerComputer computer) {
|
||||
var pos = getBlockPos();
|
||||
for (var dir : DirectionUtil.FACINGS) updateRedstoneInput(computer, dir, pos.relative(dir));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the redstone input on a particular side.
|
||||
* <p>
|
||||
* This is called <em>immediately</em> when a neighbouring block changes (see {@link #neighborChanged(BlockPos)}).
|
||||
*
|
||||
* @param computer The current server computer.
|
||||
* @param dir The direction to update in.
|
||||
* @param targetPos The position of the adjacent block, equal to {@code getBlockPos().offset(dir)}.
|
||||
*/
|
||||
private void updateRedstoneInput(ServerComputer computer, Direction dir, BlockPos targetPos) {
|
||||
var offsetSide = dir.getOpposite();
|
||||
var localDir = remapToLocalSide(dir);
|
||||
@ -211,6 +227,15 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
|
||||
computer.setBundledRedstoneInput(localDir, BundledRedstone.getOutput(getLevel(), targetPos, offsetSide));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the peripheral on a particular side.
|
||||
* <p>
|
||||
* This is called from {@link #serverTick()}, after a peripheral has been marked as invalid (such as in
|
||||
* {@link #neighborChanged(BlockPos)})
|
||||
*
|
||||
* @param computer The current server computer.
|
||||
* @param dir The direction to update in.
|
||||
*/
|
||||
private void refreshPeripheral(ServerComputer computer, Direction dir) {
|
||||
invalidSides &= ~(1 << dir.ordinal());
|
||||
|
||||
@ -243,7 +268,18 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
|
||||
}
|
||||
}
|
||||
|
||||
private void updateInputAt(BlockPos neighbour) {
|
||||
/**
|
||||
* Called when a neighbour block changes.
|
||||
* <p>
|
||||
* This finds the side the neighbour block is on, and updates the inputs accordingly.
|
||||
* <p>
|
||||
* We do <strong>NOT</strong> update the peripheral immediately. Blocks and block entities are sometimes
|
||||
* inconsistent at the point where an update is received, and so we instead just mark that side as dirty (see
|
||||
* {@link #invalidSides}) and refresh it {@linkplain #serverTick() next tick}.
|
||||
*
|
||||
* @param neighbour The position of the neighbour block.
|
||||
*/
|
||||
public void neighborChanged(BlockPos neighbour) {
|
||||
var computer = getServerComputer();
|
||||
if (computer == null) return;
|
||||
|
||||
@ -258,22 +294,39 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
|
||||
|
||||
// If the position is not any adjacent one, update all inputs. This is pretty terrible, but some redstone mods
|
||||
// handle this incorrectly.
|
||||
updateRedstoneInputs(computer);
|
||||
invalidSides = (1 << 6) - 1; // Mark all peripherals as dirty.
|
||||
for (var dir : DirectionUtil.FACINGS) updateRedstoneInput(computer, dir, getBlockPos().relative(dir));
|
||||
invalidSides = DirectionUtil.ALL_SIDES; // Mark all peripherals as dirty.
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the block's state and propagate redstone output.
|
||||
* Called when a neighbour block's shape changes.
|
||||
* <p>
|
||||
* Unlike {@link #neighborChanged(BlockPos)}, we don't update redstone, only peripherals.
|
||||
*
|
||||
* @param direction The side that changed.
|
||||
*/
|
||||
public void updateOutput() {
|
||||
BlockEntityHelpers.updateBlock(this);
|
||||
for (var dir : DirectionUtil.FACINGS) RedstoneUtil.propagateRedstoneOutput(getLevel(), getBlockPos(), dir);
|
||||
|
||||
var computer = getServerComputer();
|
||||
if (computer != null) updateRedstoneInputs(computer);
|
||||
public void neighbourShapeChanged(Direction direction) {
|
||||
invalidSides |= 1 << direction.ordinal();
|
||||
}
|
||||
|
||||
protected abstract ServerComputer createComputer(int id);
|
||||
/**
|
||||
* Update outputs in a specific direction.
|
||||
*
|
||||
* @param direction The direction to propagate outputs in.
|
||||
*/
|
||||
protected void updateRedstoneTo(Direction direction) {
|
||||
RedstoneUtil.propagateRedstoneOutput(getLevel(), getBlockPos(), direction);
|
||||
|
||||
var computer = getServerComputer();
|
||||
if (computer != null) updateRedstoneInput(computer, direction, getBlockPos().relative(direction));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all redstone outputs.
|
||||
*/
|
||||
public void updateRedstone() {
|
||||
for (var dir : DirectionUtil.FACINGS) updateRedstoneTo(dir);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int getComputerID() {
|
||||
@ -331,6 +384,8 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
|
||||
return computer;
|
||||
}
|
||||
|
||||
protected abstract ServerComputer createComputer(int id);
|
||||
|
||||
@Nullable
|
||||
public ServerComputer getServerComputer() {
|
||||
return getLevel().isClientSide || getLevel().getServer() == null ? null : ServerContext.get(getLevel().getServer()).registry().get(instanceID);
|
||||
@ -358,7 +413,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
|
||||
}
|
||||
|
||||
protected void transferStateFrom(AbstractComputerBlockEntity copy) {
|
||||
if (copy.computerID != computerID || copy.instanceID != instanceID) {
|
||||
if (copy.computerID != computerID || !Objects.equals(copy.instanceID, instanceID)) {
|
||||
unload();
|
||||
instanceID = copy.instanceID;
|
||||
computerID = copy.computerID;
|
||||
@ -368,7 +423,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
|
||||
lockCode = copy.lockCode;
|
||||
BlockEntityHelpers.updateBlock(this);
|
||||
}
|
||||
copy.instanceID = -1;
|
||||
copy.instanceID = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -51,7 +51,7 @@ public class ComputerBlockEntity extends AbstractComputerBlockEntity {
|
||||
protected void updateBlockState(ComputerState newState) {
|
||||
var existing = getBlockState();
|
||||
if (existing.getValue(ComputerBlock.STATE) != newState) {
|
||||
getLevel().setBlock(getBlockPos(), existing.setValue(ComputerBlock.STATE, newState), 3);
|
||||
getLevel().setBlock(getBlockPos(), existing.setValue(ComputerBlock.STATE, newState), ComputerBlock.UPDATE_CLIENTS);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,11 +26,13 @@ import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
private final int instanceID;
|
||||
private final UUID instanceUUID = UUID.randomUUID();
|
||||
|
||||
private ServerLevel level;
|
||||
private BlockPos position;
|
||||
@ -42,7 +44,6 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
private final NetworkedTerminal terminal;
|
||||
private final AtomicBoolean terminalChanged = new AtomicBoolean(false);
|
||||
|
||||
private boolean changedLastFrame;
|
||||
private int ticksSincePing;
|
||||
|
||||
public ServerComputer(
|
||||
@ -96,10 +97,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
|
||||
public void tickServer() {
|
||||
ticksSincePing++;
|
||||
|
||||
computer.tick();
|
||||
|
||||
changedLastFrame = computer.pollAndResetChanged();
|
||||
if (terminalChanged.getAndSet(false)) onTerminalChanged();
|
||||
}
|
||||
|
||||
@ -119,13 +117,13 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
return ticksSincePing > 100;
|
||||
}
|
||||
|
||||
public boolean hasOutputChanged() {
|
||||
return changedLastFrame;
|
||||
public int pollAndResetChanges() {
|
||||
return computer.pollAndResetChanges();
|
||||
}
|
||||
|
||||
public int register() {
|
||||
ServerContext.get(level.getServer()).registry().add(instanceID, this);
|
||||
return instanceID;
|
||||
public UUID register() {
|
||||
ServerContext.get(level.getServer()).registry().add(this);
|
||||
return instanceUUID;
|
||||
}
|
||||
|
||||
void unload() {
|
||||
@ -134,7 +132,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
|
||||
public void close() {
|
||||
unload();
|
||||
ServerContext.get(level.getServer()).registry().remove(instanceID);
|
||||
ServerContext.get(level.getServer()).registry().remove(this);
|
||||
}
|
||||
|
||||
private void sendToAllInteracting(Function<AbstractContainerMenu, NetworkMessage<ClientNetworkContext>> createPacket) {
|
||||
@ -154,6 +152,10 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
return instanceID;
|
||||
}
|
||||
|
||||
public UUID getInstanceUUID() {
|
||||
return instanceUUID;
|
||||
}
|
||||
|
||||
public int getID() {
|
||||
return computer.getID();
|
||||
}
|
||||
@ -167,7 +169,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
}
|
||||
|
||||
public ComputerState getState() {
|
||||
if (!isOn()) return ComputerState.OFF;
|
||||
if (!computer.isOn()) return ComputerState.OFF;
|
||||
return computer.isBlinking() ? ComputerState.BLINKING : ComputerState.ON;
|
||||
}
|
||||
|
||||
|
@ -8,14 +8,14 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collection;
|
||||
import java.util.Random;
|
||||
import java.util.*;
|
||||
|
||||
public class ServerComputerRegistry {
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
private final int sessionId = RANDOM.nextInt();
|
||||
private final Int2ObjectMap<ServerComputer> computers = new Int2ObjectOpenHashMap<>();
|
||||
private final Int2ObjectMap<ServerComputer> computersByInstanceId = new Int2ObjectOpenHashMap<>();
|
||||
private final Map<UUID, ServerComputer> computersByInstanceUuid = new HashMap<>();
|
||||
private int nextInstanceId;
|
||||
|
||||
public int getSessionID() {
|
||||
@ -28,11 +28,16 @@ public class ServerComputerRegistry {
|
||||
|
||||
@Nullable
|
||||
public ServerComputer get(int instanceID) {
|
||||
return instanceID >= 0 ? computers.get(instanceID) : null;
|
||||
return instanceID >= 0 ? computersByInstanceId.get(instanceID) : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ServerComputer get(int sessionId, int instanceId) {
|
||||
public ServerComputer get(@Nullable UUID instanceID) {
|
||||
return instanceID != null ? computersByInstanceUuid.get(instanceID) : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ServerComputer get(int sessionId, @Nullable UUID instanceId) {
|
||||
return sessionId == this.sessionId ? get(instanceId) : null;
|
||||
}
|
||||
|
||||
@ -50,28 +55,36 @@ public class ServerComputerRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
void add(int instanceID, ServerComputer computer) {
|
||||
remove(instanceID);
|
||||
computers.put(instanceID, computer);
|
||||
nextInstanceId = Math.max(nextInstanceId, instanceID + 1);
|
||||
}
|
||||
void add(ServerComputer computer) {
|
||||
var instanceID = computer.getInstanceID();
|
||||
var instanceUUID = computer.getInstanceUUID();
|
||||
|
||||
void remove(int instanceID) {
|
||||
var computer = get(instanceID);
|
||||
if (computer != null) {
|
||||
computer.unload();
|
||||
computer.onRemoved();
|
||||
if (computersByInstanceId.containsKey(instanceID)) {
|
||||
throw new IllegalStateException("Duplicate computer " + instanceID);
|
||||
}
|
||||
|
||||
computers.remove(instanceID);
|
||||
if (computersByInstanceUuid.containsKey(instanceUUID)) {
|
||||
throw new IllegalStateException("Duplicate computer " + instanceUUID);
|
||||
}
|
||||
|
||||
computersByInstanceId.put(instanceID, computer);
|
||||
computersByInstanceUuid.put(instanceUUID, computer);
|
||||
}
|
||||
|
||||
void remove(ServerComputer computer) {
|
||||
computer.unload();
|
||||
computer.onRemoved();
|
||||
computersByInstanceId.remove(computer.getInstanceID());
|
||||
computersByInstanceUuid.remove(computer.getInstanceUUID());
|
||||
}
|
||||
|
||||
void close() {
|
||||
for (var computer : getComputers()) computer.unload();
|
||||
computers.clear();
|
||||
computersByInstanceId.clear();
|
||||
computersByInstanceUuid.clear();
|
||||
}
|
||||
|
||||
public Collection<ServerComputer> getComputers() {
|
||||
return computers.values();
|
||||
return computersByInstanceId.values();
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ public class ViewComputerMenu extends ComputerMenuWithoutInventory {
|
||||
|
||||
private static boolean canInteractWith(ServerComputer computer, Player player) {
|
||||
// If this computer no longer exists then discard it.
|
||||
if (ServerContext.get(computer.getLevel().getServer()).registry().get(computer.getInstanceID()) != computer) {
|
||||
if (ServerContext.get(computer.getLevel().getServer()).registry().get(computer.getInstanceUUID()) != computer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import dan200.computercraft.shared.command.text.TableBuilder;
|
||||
import dan200.computercraft.shared.computer.core.ComputerState;
|
||||
import dan200.computercraft.shared.computer.terminal.TerminalState;
|
||||
import dan200.computercraft.shared.computer.upload.UploadResult;
|
||||
import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
|
||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
@ -15,7 +16,6 @@ import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.sounds.SoundEvent;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
@ -30,11 +30,11 @@ public interface ClientNetworkContext {
|
||||
|
||||
void handlePlayRecord(BlockPos pos, @Nullable SoundEvent sound, @Nullable String name);
|
||||
|
||||
void handlePocketComputerData(int instanceId, ComputerState state, int lightState, TerminalState terminal);
|
||||
void handlePocketComputerData(UUID instanceId, ComputerState state, int lightState, TerminalState terminal);
|
||||
|
||||
void handlePocketComputerDeleted(int instanceId);
|
||||
void handlePocketComputerDeleted(UUID instanceId);
|
||||
|
||||
void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, ByteBuffer audio);
|
||||
void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, EncodedAudio audio);
|
||||
|
||||
void handleSpeakerMove(UUID source, SpeakerPosition.Message position);
|
||||
|
||||
|
@ -13,24 +13,26 @@ import dan200.computercraft.shared.network.NetworkMessages;
|
||||
import dan200.computercraft.shared.pocket.core.PocketServerComputer;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Provides additional data about a client computer, such as its ID and current state.
|
||||
*/
|
||||
public class PocketComputerDataMessage implements NetworkMessage<ClientNetworkContext> {
|
||||
private final int instanceId;
|
||||
private final UUID clientId;
|
||||
private final ComputerState state;
|
||||
private final int lightState;
|
||||
private final TerminalState terminal;
|
||||
|
||||
public PocketComputerDataMessage(PocketServerComputer computer, boolean sendTerminal) {
|
||||
instanceId = computer.getInstanceID();
|
||||
clientId = computer.getInstanceUUID();
|
||||
state = computer.getState();
|
||||
lightState = computer.getLight();
|
||||
terminal = sendTerminal ? computer.getTerminalState() : new TerminalState((NetworkedTerminal) null);
|
||||
}
|
||||
|
||||
public PocketComputerDataMessage(FriendlyByteBuf buf) {
|
||||
instanceId = buf.readVarInt();
|
||||
clientId = buf.readUUID();
|
||||
state = buf.readEnum(ComputerState.class);
|
||||
lightState = buf.readVarInt();
|
||||
terminal = new TerminalState(buf);
|
||||
@ -38,7 +40,7 @@ public class PocketComputerDataMessage implements NetworkMessage<ClientNetworkCo
|
||||
|
||||
@Override
|
||||
public void write(FriendlyByteBuf buf) {
|
||||
buf.writeVarInt(instanceId);
|
||||
buf.writeUUID(clientId);
|
||||
buf.writeEnum(state);
|
||||
buf.writeVarInt(lightState);
|
||||
terminal.write(buf);
|
||||
@ -46,7 +48,7 @@ public class PocketComputerDataMessage implements NetworkMessage<ClientNetworkCo
|
||||
|
||||
@Override
|
||||
public void handle(ClientNetworkContext context) {
|
||||
context.handlePocketComputerData(instanceId, state, lightState, terminal);
|
||||
context.handlePocketComputerData(clientId, state, lightState, terminal);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -9,21 +9,23 @@ import dan200.computercraft.shared.network.NetworkMessage;
|
||||
import dan200.computercraft.shared.network.NetworkMessages;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
|
||||
public class PocketComputerDeletedClientMessage implements NetworkMessage<ClientNetworkContext> {
|
||||
private final int instanceId;
|
||||
private final UUID instanceId;
|
||||
|
||||
public PocketComputerDeletedClientMessage(int instanceId) {
|
||||
public PocketComputerDeletedClientMessage(UUID instanceId) {
|
||||
this.instanceId = instanceId;
|
||||
}
|
||||
|
||||
public PocketComputerDeletedClientMessage(FriendlyByteBuf buffer) {
|
||||
instanceId = buffer.readVarInt();
|
||||
instanceId = buffer.readUUID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(FriendlyByteBuf buf) {
|
||||
buf.writeVarInt(instanceId);
|
||||
buf.writeUUID(instanceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -7,11 +7,11 @@ package dan200.computercraft.shared.network.client;
|
||||
import dan200.computercraft.shared.network.MessageType;
|
||||
import dan200.computercraft.shared.network.NetworkMessage;
|
||||
import dan200.computercraft.shared.network.NetworkMessages;
|
||||
import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
|
||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerBlockEntity;
|
||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
@ -24,10 +24,10 @@ import java.util.UUID;
|
||||
public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkContext> {
|
||||
private final UUID source;
|
||||
private final SpeakerPosition.Message pos;
|
||||
private final ByteBuffer content;
|
||||
private final EncodedAudio content;
|
||||
private final float volume;
|
||||
|
||||
public SpeakerAudioClientMessage(UUID source, SpeakerPosition pos, float volume, ByteBuffer content) {
|
||||
public SpeakerAudioClientMessage(UUID source, SpeakerPosition pos, float volume, EncodedAudio content) {
|
||||
this.source = source;
|
||||
this.pos = pos.asMessage();
|
||||
this.content = content;
|
||||
@ -38,10 +38,7 @@ public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkCo
|
||||
source = buf.readUUID();
|
||||
pos = SpeakerPosition.Message.read(buf);
|
||||
volume = buf.readFloat();
|
||||
|
||||
var bytes = new byte[buf.readableBytes()];
|
||||
buf.readBytes(bytes);
|
||||
content = ByteBuffer.wrap(bytes);
|
||||
content = EncodedAudio.read(buf);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -49,7 +46,7 @@ public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkCo
|
||||
buf.writeUUID(source);
|
||||
pos.write(buf);
|
||||
buf.writeFloat(volume);
|
||||
buf.writeBytes(content.duplicate());
|
||||
content.write(buf);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -11,9 +11,8 @@ import dan200.computercraft.api.lua.MethodResult;
|
||||
import dan200.computercraft.api.peripheral.IComputerAccess;
|
||||
import dan200.computercraft.api.peripheral.IDynamicPeripheral;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.impl.RegistryHelper;
|
||||
import dan200.computercraft.core.computer.GuardedLuaContext;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
@ -28,13 +27,16 @@ public final class GenericPeripheral implements IDynamicPeripheral {
|
||||
private final Set<String> additionalTypes;
|
||||
private final List<SaturatedMethod> methods;
|
||||
|
||||
GenericPeripheral(BlockEntity tile, Direction side, @Nullable String name, Set<String> additionalTypes, List<SaturatedMethod> methods) {
|
||||
private @Nullable GuardedLuaContext contextWrapper;
|
||||
private final GuardedLuaContext.Guard guard;
|
||||
|
||||
GenericPeripheral(BlockEntity tile, Direction side, String type, Set<String> additionalTypes, List<SaturatedMethod> methods) {
|
||||
this.side = side;
|
||||
var type = RegistryHelper.getKeyOrThrow(BuiltInRegistries.BLOCK_ENTITY_TYPE, tile.getType());
|
||||
this.tile = tile;
|
||||
this.type = name != null ? name : type.toString();
|
||||
this.type = type;
|
||||
this.additionalTypes = additionalTypes;
|
||||
this.methods = methods;
|
||||
this.guard = () -> !tile.isRemoved() && tile.getLevel() != null && tile.getLevel().isLoaded(tile.getBlockPos());
|
||||
}
|
||||
|
||||
public Direction side() {
|
||||
@ -50,7 +52,12 @@ public final class GenericPeripheral implements IDynamicPeripheral {
|
||||
|
||||
@Override
|
||||
public MethodResult callMethod(IComputerAccess computer, ILuaContext context, int method, IArguments arguments) throws LuaException {
|
||||
return methods.get(method).apply(context, computer, arguments);
|
||||
var contextWrapper = this.contextWrapper;
|
||||
if (contextWrapper == null || !contextWrapper.wraps(context)) {
|
||||
contextWrapper = this.contextWrapper = new GuardedLuaContext(context, guard);
|
||||
}
|
||||
|
||||
return methods.get(method).apply(contextWrapper, computer, arguments);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -10,6 +10,9 @@ import dan200.computercraft.core.methods.NamedMethod;
|
||||
import dan200.computercraft.core.methods.PeripheralMethod;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
@ -25,6 +28,8 @@ import java.util.Set;
|
||||
* See the platform-specific peripheral providers for the usage of this.
|
||||
*/
|
||||
final class GenericPeripheralBuilder {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GenericPeripheralBuilder.class);
|
||||
|
||||
private @Nullable String name;
|
||||
private final Set<String> additionalTypes = new HashSet<>(0);
|
||||
private final ArrayList<SaturatedMethod> methods = new ArrayList<>();
|
||||
@ -33,8 +38,24 @@ final class GenericPeripheralBuilder {
|
||||
IPeripheral toPeripheral(BlockEntity blockEntity, Direction side) {
|
||||
if (methods.isEmpty()) return null;
|
||||
|
||||
String type;
|
||||
if (name == null) {
|
||||
var typeId = BlockEntityType.getKey(blockEntity.getType());
|
||||
if (typeId == null) {
|
||||
LOG.error(
|
||||
"Block entity {} for {} was not registered. Skipping creating a generic peripheral for it.",
|
||||
blockEntity, blockEntity.getBlockState().getBlock()
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
type = typeId.toString();
|
||||
} else {
|
||||
type = name;
|
||||
}
|
||||
|
||||
methods.trimToSize();
|
||||
return new GenericPeripheral(blockEntity, side, name, additionalTypes, methods);
|
||||
return new GenericPeripheral(blockEntity, side, type, additionalTypes, methods);
|
||||
}
|
||||
|
||||
void addMethod(Object target, String name, PeripheralMethod method, @Nullable NamedMethod<PeripheralMethod> info) {
|
||||
|
@ -127,9 +127,8 @@ public class CableBlock extends Block implements SimpleWaterloggedBlock, EntityB
|
||||
item = new ItemStack(ModRegistry.Items.CABLE.get());
|
||||
}
|
||||
|
||||
world.setBlock(pos, correctConnections(world, pos, newState), 3);
|
||||
world.setBlockAndUpdate(pos, correctConnections(world, pos, newState));
|
||||
|
||||
cable.modemChanged();
|
||||
cable.connectionsChanged();
|
||||
if (!world.isClientSide && !player.getAbilities().instabuild) {
|
||||
Block.popResource(world, pos, item);
|
||||
@ -162,10 +161,7 @@ public class CableBlock extends Block implements SimpleWaterloggedBlock, EntityB
|
||||
@Override
|
||||
public void setPlacedBy(Level world, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) {
|
||||
var tile = world.getBlockEntity(pos);
|
||||
if (tile instanceof CableBlockEntity cable) {
|
||||
if (cable.hasCable()) cable.connectionsChanged();
|
||||
}
|
||||
|
||||
if (tile instanceof CableBlockEntity cable && cable.hasCable()) cable.connectionsChanged();
|
||||
super.setPlacedBy(world, pos, state, placer, stack);
|
||||
}
|
||||
|
||||
@ -177,14 +173,37 @@ public class CableBlock extends Block implements SimpleWaterloggedBlock, EntityB
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public BlockState updateShape(BlockState state, Direction side, BlockState otherState, LevelAccessor world, BlockPos pos, BlockPos otherPos) {
|
||||
WaterloggableHelpers.updateShape(state, world, pos);
|
||||
public BlockState updateShape(BlockState state, Direction side, BlockState otherState, LevelAccessor level, BlockPos pos, BlockPos otherPos) {
|
||||
WaterloggableHelpers.updateShape(state, level, pos);
|
||||
|
||||
// Should never happen, but handle the case where we've no modem or cable.
|
||||
if (!state.getValue(CABLE) && state.getValue(MODEM) == CableModemVariant.None) {
|
||||
return getFluidState(state).createLegacyBlock();
|
||||
}
|
||||
|
||||
return world instanceof Level level ? state.setValue(CONNECTIONS.get(side), doesConnectVisually(state, level, pos, side)) : state;
|
||||
// Pop our modem if needed.
|
||||
var dir = state.getValue(MODEM).getFacing();
|
||||
if (dir != null && dir.equals(side) && !canSupportCenter(level, otherPos, side.getOpposite())) {
|
||||
// If we've no cable, follow normal Minecraft logic and just remove the block.
|
||||
if (!state.getValue(CABLE)) return getFluidState(state).createLegacyBlock();
|
||||
|
||||
// Otherwise remove the cable and drop the modem manually.
|
||||
state = state.setValue(CableBlock.MODEM, CableModemVariant.None);
|
||||
if (level instanceof Level actualLevel) {
|
||||
Block.popResource(actualLevel, pos, new ItemStack(ModRegistry.Items.WIRED_MODEM.get()));
|
||||
}
|
||||
|
||||
if (level.getBlockEntity(pos) instanceof CableBlockEntity cable) cable.scheduleConnectionsChanged();
|
||||
}
|
||||
|
||||
var modem = state.getValue(MODEM);
|
||||
if (modem.getFacing() == side && modem.isPeripheralOn() && level.getBlockEntity(pos) instanceof CableBlockEntity cable) {
|
||||
cable.queueRefreshPeripheral();
|
||||
}
|
||||
|
||||
return level instanceof Level actualLevel
|
||||
? state.setValue(CONNECTIONS.get(side), doesConnectVisually(state, actualLevel, pos, side))
|
||||
: state;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -230,6 +249,7 @@ public class CableBlock extends Block implements SimpleWaterloggedBlock, EntityB
|
||||
@Override
|
||||
@Deprecated
|
||||
public final InteractionResult use(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) {
|
||||
if (player.isCrouching() || !player.mayBuild()) return InteractionResult.PASS;
|
||||
return world.getBlockEntity(pos) instanceof CableBlockEntity modem ? modem.use(player) : InteractionResult.PASS;
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,6 @@ package dan200.computercraft.shared.peripheral.modem.wired;
|
||||
import dan200.computercraft.api.network.wired.WiredElement;
|
||||
import dan200.computercraft.api.network.wired.WiredNode;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.command.text.ChatHelpers;
|
||||
import dan200.computercraft.shared.peripheral.modem.ModemState;
|
||||
import dan200.computercraft.shared.platform.ComponentAccess;
|
||||
@ -20,21 +19,16 @@ import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
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.BlockEntityType;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public class CableBlockEntity extends BlockEntity {
|
||||
private static final String NBT_PERIPHERAL_ENABLED = "PeripheralAccess";
|
||||
|
||||
private final class CableElement extends WiredModemElement {
|
||||
@Override
|
||||
public Level getLevel() {
|
||||
@ -57,33 +51,21 @@ public class CableBlockEntity extends BlockEntity {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean invalidPeripheral;
|
||||
private boolean peripheralAccessAllowed;
|
||||
private boolean refreshPeripheral;
|
||||
private final WiredModemLocalPeripheral peripheral = new WiredModemLocalPeripheral(PlatformHelper.get().createPeripheralAccess(this, x -> queueRefreshPeripheral()));
|
||||
|
||||
private boolean connectionsFormed = false;
|
||||
private boolean connectionsChanged = false;
|
||||
private boolean refreshConnections = false;
|
||||
|
||||
private final WiredModemElement cable = new CableElement();
|
||||
private final WiredNode node = cable.getNode();
|
||||
private final TickScheduler.Token tickToken = new TickScheduler.Token(this);
|
||||
private final WiredModemPeripheral modem = new WiredModemPeripheral(
|
||||
new ModemState(() -> TickScheduler.schedule(tickToken)),
|
||||
cable
|
||||
new ModemState(() -> TickScheduler.schedule(tickToken)), cable, peripheral, this
|
||||
) {
|
||||
@Override
|
||||
protected WiredModemLocalPeripheral getLocalPeripheral() {
|
||||
return peripheral;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 getPosition() {
|
||||
return Vec3.atCenterOf(getBlockPos().relative(getDirection()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getTarget() {
|
||||
return CableBlockEntity.this;
|
||||
var dir = getModemDirection();
|
||||
return Vec3.atCenterOf(dir == null ? getBlockPos() : getBlockPos().relative(dir));
|
||||
}
|
||||
};
|
||||
|
||||
@ -93,93 +75,61 @@ public class CableBlockEntity extends BlockEntity {
|
||||
super(type, pos, state);
|
||||
}
|
||||
|
||||
private void onRemove() {
|
||||
if (level == null || !level.isClientSide) {
|
||||
node.remove();
|
||||
connectionsFormed = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRemoved() {
|
||||
super.setRemoved();
|
||||
modem.removed();
|
||||
onRemove();
|
||||
if (level == null || !level.isClientSide) node.remove();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearRemoved() {
|
||||
super.clearRemoved();
|
||||
refreshConnections = refreshPeripheral = true;
|
||||
TickScheduler.schedule(tickToken);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public void setBlockState(BlockState state) {
|
||||
var direction = getMaybeDirection();
|
||||
var direction = getModemDirection();
|
||||
var hasCable = hasCable();
|
||||
super.setBlockState(state);
|
||||
|
||||
// We invalidate both the modem and element if the modem's direction is different.
|
||||
if (getMaybeDirection() != direction) PlatformHelper.get().invalidateComponent(this);
|
||||
// We invalidate both the modem and element if the modem direction or cable are different.
|
||||
if (hasCable() != hasCable || getModemDirection() != direction) PlatformHelper.get().invalidateComponent(this);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Direction getMaybeDirection() {
|
||||
private Direction getModemDirection() {
|
||||
return getBlockState().getValue(CableBlock.MODEM).getFacing();
|
||||
}
|
||||
|
||||
private Direction getDirection() {
|
||||
var direction = getMaybeDirection();
|
||||
return direction == null ? Direction.NORTH : direction;
|
||||
}
|
||||
|
||||
void neighborChanged(BlockPos neighbour) {
|
||||
var dir = getDirection();
|
||||
if (neighbour.equals(getBlockPos().relative(dir)) && hasModem() && !getBlockState().canSurvive(getLevel(), getBlockPos())) {
|
||||
if (hasCable()) {
|
||||
// Drop the modem and convert to cable
|
||||
Block.popResource(getLevel(), getBlockPos(), new ItemStack(ModRegistry.Items.WIRED_MODEM.get()));
|
||||
getLevel().setBlockAndUpdate(getBlockPos(), getBlockState().setValue(CableBlock.MODEM, CableModemVariant.None));
|
||||
modemChanged();
|
||||
connectionsChanged();
|
||||
} else {
|
||||
// Drop everything and remove block
|
||||
Block.popResource(getLevel(), getBlockPos(), new ItemStack(ModRegistry.Items.WIRED_MODEM.get()));
|
||||
getLevel().removeBlock(getBlockPos(), false);
|
||||
// This'll call #destroy(), so we don't need to reset the network here.
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!level.isClientSide && peripheralAccessAllowed) {
|
||||
var facing = getDirection();
|
||||
if (getBlockPos().relative(facing).equals(neighbour)) queueRefreshPeripheral();
|
||||
var dir = getModemDirection();
|
||||
if (!level.isClientSide && dir != null && getBlockPos().relative(dir).equals(neighbour) && isPeripheralOn()) {
|
||||
queueRefreshPeripheral();
|
||||
}
|
||||
}
|
||||
|
||||
private void queueRefreshPeripheral() {
|
||||
if (invalidPeripheral) return;
|
||||
invalidPeripheral = true;
|
||||
void queueRefreshPeripheral() {
|
||||
refreshPeripheral = true;
|
||||
TickScheduler.schedule(tickToken);
|
||||
}
|
||||
|
||||
private void refreshPeripheral() {
|
||||
invalidPeripheral = false;
|
||||
if (level != null && !isRemoved() && peripheral.attach(level, getBlockPos(), getDirection())) {
|
||||
updateConnectedPeripherals();
|
||||
}
|
||||
}
|
||||
|
||||
InteractionResult use(Player player) {
|
||||
if (player.isCrouching() || !player.mayBuild()) return InteractionResult.PASS;
|
||||
if (!canAttachPeripheral()) return InteractionResult.FAIL;
|
||||
|
||||
if (getLevel().isClientSide) return InteractionResult.SUCCESS;
|
||||
|
||||
var oldName = peripheral.getConnectedName();
|
||||
togglePeripheralAccess();
|
||||
if (isPeripheralOn()) {
|
||||
detachPeripheral();
|
||||
} else {
|
||||
attachPeripheral();
|
||||
}
|
||||
var newName = peripheral.getConnectedName();
|
||||
|
||||
if (!Objects.equals(newName, oldName)) {
|
||||
if (oldName != null) {
|
||||
player.displayClientMessage(Component.translatable("chat.computercraft.wired_modem.peripheral_disconnected",
|
||||
@ -197,14 +147,11 @@ public class CableBlockEntity extends BlockEntity {
|
||||
@Override
|
||||
public void load(CompoundTag nbt) {
|
||||
super.load(nbt);
|
||||
// Fallback to the previous (incorrect) key
|
||||
peripheralAccessAllowed = nbt.getBoolean(NBT_PERIPHERAL_ENABLED) || nbt.getBoolean("PeirpheralAccess");
|
||||
peripheral.read(nbt, "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveAdditional(CompoundTag nbt) {
|
||||
nbt.putBoolean(NBT_PERIPHERAL_ENABLED, peripheralAccessAllowed);
|
||||
peripheral.write(nbt, "");
|
||||
super.saveAdditional(nbt);
|
||||
}
|
||||
@ -213,7 +160,7 @@ public class CableBlockEntity extends BlockEntity {
|
||||
var state = getBlockState();
|
||||
var oldVariant = state.getValue(CableBlock.MODEM);
|
||||
var newVariant = CableModemVariant
|
||||
.from(oldVariant.getFacing(), modem.getModemState().isOpen(), peripheralAccessAllowed);
|
||||
.from(oldVariant.getFacing(), modem.getModemState().isOpen(), peripheral.hasPeripheral());
|
||||
|
||||
if (oldVariant != newVariant) {
|
||||
level.setBlockAndUpdate(getBlockPos(), state.setValue(CableBlock.MODEM, newVariant));
|
||||
@ -223,31 +170,24 @@ public class CableBlockEntity extends BlockEntity {
|
||||
void blockTick() {
|
||||
if (getLevel().isClientSide) return;
|
||||
|
||||
if (invalidPeripheral) refreshPeripheral();
|
||||
if (refreshPeripheral) {
|
||||
refreshPeripheral = false;
|
||||
if (isPeripheralOn()) attachPeripheral();
|
||||
}
|
||||
|
||||
if (modem.getModemState().pollChanged()) updateBlockState();
|
||||
|
||||
if (!connectionsFormed) {
|
||||
connectionsFormed = true;
|
||||
|
||||
connectionsChanged();
|
||||
if (peripheralAccessAllowed) {
|
||||
peripheral.attach(level, worldPosition, getDirection());
|
||||
updateConnectedPeripherals();
|
||||
}
|
||||
}
|
||||
|
||||
if (connectionsChanged) connectionsChanged();
|
||||
if (refreshConnections) connectionsChanged();
|
||||
}
|
||||
|
||||
private void scheduleConnectionsChanged() {
|
||||
connectionsChanged = true;
|
||||
void scheduleConnectionsChanged() {
|
||||
refreshConnections = true;
|
||||
TickScheduler.schedule(tickToken);
|
||||
}
|
||||
|
||||
void connectionsChanged() {
|
||||
if (getLevel().isClientSide) return;
|
||||
connectionsChanged = false;
|
||||
refreshConnections = false;
|
||||
|
||||
var state = getBlockState();
|
||||
var world = getLevel();
|
||||
@ -263,56 +203,29 @@ public class CableBlockEntity extends BlockEntity {
|
||||
if (CableBlock.canConnectIn(state, facing)) {
|
||||
// If we can connect to it then do so
|
||||
this.node.connectTo(node);
|
||||
} else if (this.node.getNetwork() == node.getNetwork()) {
|
||||
// Otherwise if we're on the same network then attempt to void it.
|
||||
} else {
|
||||
// Otherwise break the connection.
|
||||
this.node.disconnectFrom(node);
|
||||
}
|
||||
}
|
||||
|
||||
// If we can no longer attach peripherals, then detach any which may have existed
|
||||
if (!canAttachPeripheral()) detachPeripheral();
|
||||
}
|
||||
|
||||
void modemChanged() {
|
||||
// Tell anyone who cares that the connection state has changed
|
||||
PlatformHelper.get().invalidateComponent(this);
|
||||
|
||||
if (getLevel().isClientSide) return;
|
||||
|
||||
// If we can no longer attach peripherals, then detach any
|
||||
// which may have existed
|
||||
if (!canAttachPeripheral() && peripheralAccessAllowed) {
|
||||
peripheralAccessAllowed = false;
|
||||
peripheral.detach();
|
||||
node.updatePeripherals(Map.of());
|
||||
setChanged();
|
||||
updateBlockState();
|
||||
}
|
||||
private void attachPeripheral() {
|
||||
var dir = Objects.requireNonNull(getModemDirection(), "Attaching without a modem");
|
||||
if (peripheral.attach(getLevel(), getBlockPos(), dir)) updateConnectedPeripherals();
|
||||
updateBlockState();
|
||||
}
|
||||
|
||||
private void togglePeripheralAccess() {
|
||||
if (!peripheralAccessAllowed) {
|
||||
peripheral.attach(level, getBlockPos(), getDirection());
|
||||
if (!peripheral.hasPeripheral()) return;
|
||||
|
||||
peripheralAccessAllowed = true;
|
||||
node.updatePeripherals(peripheral.toMap());
|
||||
} else {
|
||||
peripheral.detach();
|
||||
|
||||
peripheralAccessAllowed = false;
|
||||
node.updatePeripherals(Map.of());
|
||||
}
|
||||
|
||||
private void detachPeripheral() {
|
||||
if (peripheral.detach()) updateConnectedPeripherals();
|
||||
updateBlockState();
|
||||
}
|
||||
|
||||
private void updateConnectedPeripherals() {
|
||||
var peripherals = peripheral.toMap();
|
||||
if (peripherals.isEmpty()) {
|
||||
// If there are no peripherals then disable access and update the display state.
|
||||
peripheralAccessAllowed = false;
|
||||
updateBlockState();
|
||||
}
|
||||
|
||||
node.updatePeripherals(peripherals);
|
||||
node.updatePeripherals(peripheral.toMap());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@ -322,7 +235,11 @@ public class CableBlockEntity extends BlockEntity {
|
||||
|
||||
@Nullable
|
||||
public IPeripheral getPeripheral(@Nullable Direction direction) {
|
||||
return direction == null || getMaybeDirection() == direction ? modem : null;
|
||||
return direction == null || getModemDirection() == direction ? modem : null;
|
||||
}
|
||||
|
||||
private boolean isPeripheralOn() {
|
||||
return getBlockState().getValue(CableBlock.MODEM).isPeripheralOn();
|
||||
}
|
||||
|
||||
boolean hasCable() {
|
||||
|
@ -31,15 +31,12 @@ public abstract class CableBlockItem extends BlockItem {
|
||||
// TODO: Check entity collision.
|
||||
if (!state.canSurvive(world, pos)) return false;
|
||||
|
||||
world.setBlock(pos, state, 3);
|
||||
world.setBlockAndUpdate(pos, state);
|
||||
var soundType = state.getBlock().getSoundType(state);
|
||||
world.playSound(null, pos, soundType.getPlaceSound(), SoundSource.BLOCKS, (soundType.getVolume() + 1.0F) / 2.0F, soundType.getPitch() * 0.8F);
|
||||
|
||||
var tile = world.getBlockEntity(pos);
|
||||
if (tile instanceof CableBlockEntity cable) {
|
||||
cable.modemChanged();
|
||||
cable.connectionsChanged();
|
||||
}
|
||||
if (tile instanceof CableBlockEntity cable) cable.connectionsChanged();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -10,49 +10,57 @@ import net.minecraft.util.StringRepresentable;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public enum CableModemVariant implements StringRepresentable {
|
||||
None("none", null),
|
||||
DownOff("down_off", Direction.DOWN),
|
||||
UpOff("up_off", Direction.UP),
|
||||
NorthOff("north_off", Direction.NORTH),
|
||||
SouthOff("south_off", Direction.SOUTH),
|
||||
WestOff("west_off", Direction.WEST),
|
||||
EastOff("east_off", Direction.EAST),
|
||||
DownOn("down_on", Direction.DOWN),
|
||||
UpOn("up_on", Direction.UP),
|
||||
NorthOn("north_on", Direction.NORTH),
|
||||
SouthOn("south_on", Direction.SOUTH),
|
||||
WestOn("west_on", Direction.WEST),
|
||||
EastOn("east_on", Direction.EAST),
|
||||
DownOffPeripheral("down_off_peripheral", Direction.DOWN),
|
||||
UpOffPeripheral("up_off_peripheral", Direction.UP),
|
||||
NorthOffPeripheral("north_off_peripheral", Direction.NORTH),
|
||||
SouthOffPeripheral("south_off_peripheral", Direction.SOUTH),
|
||||
WestOffPeripheral("west_off_peripheral", Direction.WEST),
|
||||
EastOffPeripheral("east_off_peripheral", Direction.EAST),
|
||||
DownOnPeripheral("down_on_peripheral", Direction.DOWN),
|
||||
UpOnPeripheral("up_on_peripheral", Direction.UP),
|
||||
NorthOnPeripheral("north_on_peripheral", Direction.NORTH),
|
||||
SouthOnPeripheral("south_on_peripheral", Direction.SOUTH),
|
||||
WestOnPeripheral("west_on_peripheral", Direction.WEST),
|
||||
EastOnPeripheral("east_on_peripheral", Direction.EAST);
|
||||
None("none", null, false, false),
|
||||
DownOff("down_off", Direction.DOWN, false, false),
|
||||
UpOff("up_off", Direction.UP, false, false),
|
||||
NorthOff("north_off", Direction.NORTH, false, false),
|
||||
SouthOff("south_off", Direction.SOUTH, false, false),
|
||||
WestOff("west_off", Direction.WEST, false, false),
|
||||
EastOff("east_off", Direction.EAST, false, false),
|
||||
DownOn("down_on", Direction.DOWN, true, false),
|
||||
UpOn("up_on", Direction.UP, true, false),
|
||||
NorthOn("north_on", Direction.NORTH, true, false),
|
||||
SouthOn("south_on", Direction.SOUTH, true, false),
|
||||
WestOn("west_on", Direction.WEST, true, false),
|
||||
EastOn("east_on", Direction.EAST, true, false),
|
||||
DownOffPeripheral("down_off_peripheral", Direction.DOWN, false, true),
|
||||
UpOffPeripheral("up_off_peripheral", Direction.UP, false, true),
|
||||
NorthOffPeripheral("north_off_peripheral", Direction.NORTH, false, true),
|
||||
SouthOffPeripheral("south_off_peripheral", Direction.SOUTH, false, true),
|
||||
WestOffPeripheral("west_off_peripheral", Direction.WEST, false, true),
|
||||
EastOffPeripheral("east_off_peripheral", Direction.EAST, false, true),
|
||||
DownOnPeripheral("down_on_peripheral", Direction.DOWN, true, true),
|
||||
UpOnPeripheral("up_on_peripheral", Direction.UP, true, true),
|
||||
NorthOnPeripheral("north_on_peripheral", Direction.NORTH, true, true),
|
||||
SouthOnPeripheral("south_on_peripheral", Direction.SOUTH, true, true),
|
||||
WestOnPeripheral("west_on_peripheral", Direction.WEST, true, true),
|
||||
EastOnPeripheral("east_on_peripheral", Direction.EAST, true, true);
|
||||
|
||||
private static final CableModemVariant[] VALUES = values();
|
||||
|
||||
private final String name;
|
||||
private final @Nullable Direction facing;
|
||||
private final boolean modemOn, peripheralOn;
|
||||
|
||||
CableModemVariant(String name, @Nullable Direction facing) {
|
||||
CableModemVariant(String name, @Nullable Direction facing, boolean modemOn, boolean peripheralOn) {
|
||||
this.name = name;
|
||||
this.facing = facing;
|
||||
this.modemOn = modemOn;
|
||||
this.peripheralOn = peripheralOn;
|
||||
if (ordinal() != getIndex(facing, modemOn, peripheralOn)) throw new IllegalStateException("Mismatched ordinal");
|
||||
}
|
||||
|
||||
public static CableModemVariant from(Direction facing) {
|
||||
return facing == null ? None : VALUES[1 + facing.get3DDataValue()];
|
||||
return VALUES[1 + facing.get3DDataValue()];
|
||||
}
|
||||
|
||||
private static int getIndex(@Nullable Direction facing, boolean modem, boolean peripheral) {
|
||||
var state = (modem ? 1 : 0) + (peripheral ? 2 : 0);
|
||||
return facing == null ? 0 : 1 + 6 * state + facing.get3DDataValue();
|
||||
}
|
||||
|
||||
public static CableModemVariant from(@Nullable Direction facing, boolean modem, boolean peripheral) {
|
||||
var state = (modem ? 1 : 0) + (peripheral ? 2 : 0);
|
||||
return facing == null ? None : VALUES[1 + 6 * state + facing.get3DDataValue()];
|
||||
return VALUES[getIndex(facing, modem, peripheral)];
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -64,6 +72,14 @@ public enum CableModemVariant implements StringRepresentable {
|
||||
return facing;
|
||||
}
|
||||
|
||||
public boolean isModemOn() {
|
||||
return modemOn;
|
||||
}
|
||||
|
||||
public boolean isPeripheralOn() {
|
||||
return peripheralOn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
|
@ -7,12 +7,14 @@ package dan200.computercraft.shared.peripheral.modem.wired;
|
||||
import dan200.computercraft.annotations.ForgeOverride;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelAccessor;
|
||||
import net.minecraft.world.level.LevelReader;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.EntityBlock;
|
||||
@ -49,13 +51,27 @@ public class WiredModemFullBlock extends Block implements EntityBlock {
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public final void neighborChanged(BlockState state, Level world, BlockPos pos, Block neighbourBlock, BlockPos neighbourPos, boolean isMoving) {
|
||||
if (world.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) modem.neighborChanged(neighbourPos);
|
||||
public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor level, BlockPos pos, BlockPos neighborPos) {
|
||||
if (state.getValue(PERIPHERAL_ON) && level.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) {
|
||||
modem.queueRefreshPeripheral(direction);
|
||||
}
|
||||
|
||||
return super.updateShape(state, direction, neighborState, level, pos, neighborPos);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public final void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighbourBlock, BlockPos neighbourPos, boolean isMoving) {
|
||||
if (state.getValue(PERIPHERAL_ON) && level.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) {
|
||||
modem.neighborChanged(neighbourPos);
|
||||
}
|
||||
}
|
||||
|
||||
@ForgeOverride
|
||||
public final void onNeighborChange(BlockState state, LevelReader world, BlockPos pos, BlockPos neighbour) {
|
||||
if (world.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) modem.neighborChanged(neighbour);
|
||||
public final void onNeighborChange(BlockState state, LevelReader level, BlockPos pos, BlockPos neighbour) {
|
||||
if (state.getValue(PERIPHERAL_ON) && level.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) {
|
||||
modem.neighborChanged(neighbour);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -32,8 +32,6 @@ import static dan200.computercraft.shared.peripheral.modem.wired.WiredModemFullB
|
||||
import static dan200.computercraft.shared.peripheral.modem.wired.WiredModemFullBlock.PERIPHERAL_ON;
|
||||
|
||||
public class WiredModemFullBlockEntity extends BlockEntity {
|
||||
private static final String NBT_PERIPHERAL_ENABLED = "PeripheralAccess";
|
||||
|
||||
private static final class FullElement extends WiredModemElement {
|
||||
private final WiredModemFullBlockEntity entity;
|
||||
|
||||
@ -70,11 +68,9 @@ public class WiredModemFullBlockEntity extends BlockEntity {
|
||||
|
||||
private final WiredModemPeripheral[] modems = new WiredModemPeripheral[6];
|
||||
|
||||
private boolean peripheralAccessAllowed = false;
|
||||
private final WiredModemLocalPeripheral[] peripherals = new WiredModemLocalPeripheral[6];
|
||||
|
||||
private boolean connectionsFormed = false;
|
||||
private boolean connectionsChanged = false;
|
||||
private boolean refreshConnections = false;
|
||||
|
||||
private final TickScheduler.Token tickToken = new TickScheduler.Token(this);
|
||||
private final ModemState modemState = new ModemState(() -> TickScheduler.schedule(tickToken));
|
||||
@ -96,31 +92,30 @@ public class WiredModemFullBlockEntity extends BlockEntity {
|
||||
@Override
|
||||
public void setRemoved() {
|
||||
super.setRemoved();
|
||||
if (level == null || !level.isClientSide) {
|
||||
node.remove();
|
||||
connectionsFormed = false;
|
||||
|
||||
for (var modem : modems) {
|
||||
if (modem != null) modem.removed();
|
||||
}
|
||||
if (level == null || !level.isClientSide) node.remove();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearRemoved() {
|
||||
super.clearRemoved();
|
||||
refreshConnections = true;
|
||||
invalidSides = DirectionUtil.ALL_SIDES;
|
||||
TickScheduler.schedule(tickToken);
|
||||
}
|
||||
|
||||
void neighborChanged(BlockPos neighbour) {
|
||||
if (!level.isClientSide && peripheralAccessAllowed) {
|
||||
for (var facing : DirectionUtil.FACINGS) {
|
||||
if (getBlockPos().relative(facing).equals(neighbour)) queueRefreshPeripheral(facing);
|
||||
}
|
||||
for (var facing : DirectionUtil.FACINGS) {
|
||||
if (getBlockPos().relative(facing).equals(neighbour)) queueRefreshPeripheral(facing);
|
||||
}
|
||||
}
|
||||
|
||||
private void queueRefreshPeripheral(Direction facing) {
|
||||
if (invalidSides == 0) TickScheduler.schedule(tickToken);
|
||||
void queueRefreshPeripheral(Direction facing) {
|
||||
invalidSides |= 1 << facing.ordinal();
|
||||
}
|
||||
|
||||
private void refreshPeripheral(Direction facing) {
|
||||
invalidSides &= ~(1 << facing.ordinal());
|
||||
var peripheral = peripherals[facing.ordinal()];
|
||||
if (level != null && !isRemoved() && peripheral.attach(level, getBlockPos(), facing)) {
|
||||
updateConnectedPeripherals();
|
||||
}
|
||||
TickScheduler.schedule(tickToken);
|
||||
}
|
||||
|
||||
public InteractionResult use(Player player) {
|
||||
@ -129,7 +124,11 @@ public class WiredModemFullBlockEntity extends BlockEntity {
|
||||
|
||||
// On server, we interacted if a peripheral was found
|
||||
var oldPeriphNames = getConnectedPeripheralNames();
|
||||
togglePeripheralAccess();
|
||||
if (isPeripheralOn()) {
|
||||
detachPeripherals();
|
||||
} else {
|
||||
attachPeripherals(DirectionUtil.ALL_SIDES);
|
||||
}
|
||||
var periphNames = getConnectedPeripheralNames();
|
||||
|
||||
if (!Objects.equals(periphNames, oldPeriphNames)) {
|
||||
@ -158,65 +157,45 @@ public class WiredModemFullBlockEntity extends BlockEntity {
|
||||
@Override
|
||||
public void load(CompoundTag nbt) {
|
||||
super.load(nbt);
|
||||
peripheralAccessAllowed = nbt.getBoolean(NBT_PERIPHERAL_ENABLED);
|
||||
for (var i = 0; i < peripherals.length; i++) peripherals[i].read(nbt, Integer.toString(i));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveAdditional(CompoundTag nbt) {
|
||||
nbt.putBoolean(NBT_PERIPHERAL_ENABLED, peripheralAccessAllowed);
|
||||
for (var i = 0; i < peripherals.length; i++) peripherals[i].write(nbt, Integer.toString(i));
|
||||
super.saveAdditional(nbt);
|
||||
}
|
||||
|
||||
private void updateBlockState() {
|
||||
var state = getBlockState();
|
||||
boolean modemOn = modemState.isOpen(), peripheralOn = peripheralAccessAllowed;
|
||||
if (state.getValue(MODEM_ON) == modemOn && state.getValue(PERIPHERAL_ON) == peripheralOn) return;
|
||||
|
||||
getLevel().setBlockAndUpdate(getBlockPos(), state.setValue(MODEM_ON, modemOn).setValue(PERIPHERAL_ON, peripheralOn));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearRemoved() {
|
||||
super.clearRemoved();
|
||||
TickScheduler.schedule(tickToken);
|
||||
}
|
||||
|
||||
void blockTick() {
|
||||
if (getLevel().isClientSide) return;
|
||||
|
||||
if (invalidSides != 0) {
|
||||
for (var direction : DirectionUtil.FACINGS) {
|
||||
if ((invalidSides & (1 << direction.ordinal())) != 0) refreshPeripheral(direction);
|
||||
}
|
||||
var oldInvalidSides = invalidSides;
|
||||
invalidSides = 0;
|
||||
if (isPeripheralOn()) attachPeripherals(oldInvalidSides);
|
||||
}
|
||||
|
||||
if (modemState.pollChanged()) updateBlockState();
|
||||
if (modemState.pollChanged()) updateModemBlockState();
|
||||
|
||||
if (!connectionsFormed) {
|
||||
connectionsFormed = true;
|
||||
if (refreshConnections) connectionsChanged();
|
||||
}
|
||||
|
||||
connectionsChanged();
|
||||
if (peripheralAccessAllowed) {
|
||||
for (var facing : DirectionUtil.FACINGS) {
|
||||
peripherals[facing.ordinal()].attach(level, getBlockPos(), facing);
|
||||
}
|
||||
updateConnectedPeripherals();
|
||||
}
|
||||
}
|
||||
private void updateModemBlockState() {
|
||||
var state = getBlockState();
|
||||
var modemOn = modemState.isOpen();
|
||||
if (state.getValue(MODEM_ON) == modemOn) return;
|
||||
|
||||
if (connectionsChanged) connectionsChanged();
|
||||
getLevel().setBlockAndUpdate(getBlockPos(), state.setValue(MODEM_ON, modemOn));
|
||||
}
|
||||
|
||||
private void scheduleConnectionsChanged() {
|
||||
connectionsChanged = true;
|
||||
refreshConnections = true;
|
||||
TickScheduler.schedule(tickToken);
|
||||
}
|
||||
|
||||
private void connectionsChanged() {
|
||||
if (getLevel().isClientSide) return;
|
||||
connectionsChanged = false;
|
||||
refreshConnections = false;
|
||||
|
||||
var world = getLevel();
|
||||
var current = getBlockPos();
|
||||
@ -231,57 +210,48 @@ public class WiredModemFullBlockEntity extends BlockEntity {
|
||||
}
|
||||
}
|
||||
|
||||
private void togglePeripheralAccess() {
|
||||
if (!peripheralAccessAllowed) {
|
||||
var hasAny = false;
|
||||
for (var facing : DirectionUtil.FACINGS) {
|
||||
var peripheral = peripherals[facing.ordinal()];
|
||||
peripheral.attach(level, getBlockPos(), facing);
|
||||
hasAny |= peripheral.hasPeripheral();
|
||||
}
|
||||
|
||||
if (!hasAny) return;
|
||||
|
||||
peripheralAccessAllowed = true;
|
||||
node.updatePeripherals(getConnectedPeripherals());
|
||||
} else {
|
||||
peripheralAccessAllowed = false;
|
||||
|
||||
for (var peripheral : peripherals) peripheral.detach();
|
||||
node.updatePeripherals(Map.of());
|
||||
}
|
||||
|
||||
updateBlockState();
|
||||
}
|
||||
|
||||
private Set<String> getConnectedPeripheralNames() {
|
||||
if (!peripheralAccessAllowed) return Set.of();
|
||||
|
||||
Set<String> peripherals = new HashSet<>(6);
|
||||
private List<String> getConnectedPeripheralNames() {
|
||||
List<String> peripherals = new ArrayList<>(6);
|
||||
for (var peripheral : this.peripherals) {
|
||||
var name = peripheral.getConnectedName();
|
||||
if (name != null) peripherals.add(name);
|
||||
}
|
||||
peripherals.sort(String::compareTo);
|
||||
return peripherals;
|
||||
}
|
||||
|
||||
private Map<String, IPeripheral> getConnectedPeripherals() {
|
||||
if (!peripheralAccessAllowed) return Map.of();
|
||||
private void attachPeripherals(int sides) {
|
||||
var anyChanged = false;
|
||||
|
||||
Map<String, IPeripheral> peripherals = new HashMap<>(6);
|
||||
for (var peripheral : this.peripherals) peripheral.extendMap(peripherals);
|
||||
return Collections.unmodifiableMap(peripherals);
|
||||
}
|
||||
Map<String, IPeripheral> attachedPeripherals = new HashMap<>(6);
|
||||
|
||||
private void updateConnectedPeripherals() {
|
||||
var peripherals = getConnectedPeripherals();
|
||||
if (peripherals.isEmpty()) {
|
||||
// If there are no peripherals then disable access and update the display state.
|
||||
peripheralAccessAllowed = false;
|
||||
updateBlockState();
|
||||
for (var facing : DirectionUtil.FACINGS) {
|
||||
var peripheral = peripherals[facing.ordinal()];
|
||||
if (DirectionUtil.isSet(sides, facing)) anyChanged |= peripheral.attach(getLevel(), getBlockPos(), facing);
|
||||
peripheral.extendMap(attachedPeripherals);
|
||||
}
|
||||
|
||||
node.updatePeripherals(peripherals);
|
||||
if (anyChanged) node.updatePeripherals(attachedPeripherals);
|
||||
|
||||
updatePeripheralBlocKState(!attachedPeripherals.isEmpty());
|
||||
}
|
||||
|
||||
private void detachPeripherals() {
|
||||
var anyChanged = false;
|
||||
for (var peripheral : peripherals) anyChanged |= peripheral.detach();
|
||||
if (anyChanged) node.updatePeripherals(Map.of());
|
||||
|
||||
updatePeripheralBlocKState(false);
|
||||
}
|
||||
|
||||
private void updatePeripheralBlocKState(boolean peripheralOn) {
|
||||
var state = getBlockState();
|
||||
if (state.getValue(PERIPHERAL_ON) == peripheralOn) return;
|
||||
getLevel().setBlockAndUpdate(getBlockPos(), state.setValue(PERIPHERAL_ON, peripheralOn));
|
||||
}
|
||||
|
||||
private boolean isPeripheralOn() {
|
||||
return getBlockState().getValue(PERIPHERAL_ON);
|
||||
}
|
||||
|
||||
public WiredElement getElement() {
|
||||
@ -295,22 +265,11 @@ public class WiredModemFullBlockEntity extends BlockEntity {
|
||||
var peripheral = modems[side.ordinal()];
|
||||
if (peripheral != null) return peripheral;
|
||||
|
||||
var localPeripheral = peripherals[side.ordinal()];
|
||||
return modems[side.ordinal()] = new WiredModemPeripheral(modemState, element) {
|
||||
@Override
|
||||
protected WiredModemLocalPeripheral getLocalPeripheral() {
|
||||
return localPeripheral;
|
||||
}
|
||||
|
||||
return modems[side.ordinal()] = new WiredModemPeripheral(modemState, element, peripherals[side.ordinal()], this) {
|
||||
@Override
|
||||
public Vec3 getPosition() {
|
||||
return Vec3.atCenterOf(getBlockPos().relative(side));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getTarget() {
|
||||
return WiredModemFullBlockEntity.this;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ package dan200.computercraft.shared.peripheral.modem.wired;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftTags;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.core.util.PeripheralHelpers;
|
||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||
import dan200.computercraft.shared.platform.ComponentAccess;
|
||||
import net.minecraft.core.BlockPos;
|
||||
@ -15,7 +16,6 @@ import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import static dan200.computercraft.core.util.Nullability.assertNonNull;
|
||||
@ -66,7 +66,7 @@ public final class WiredModemLocalPeripheral {
|
||||
this.id = ServerContext.get(assertNonNull(world.getServer())).getNextId("peripheral." + type);
|
||||
}
|
||||
|
||||
return oldPeripheral == null || !oldPeripheral.equals(peripheral);
|
||||
return !PeripheralHelpers.equals(oldPeripheral, peripheral);
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,11 +86,6 @@ public final class WiredModemLocalPeripheral {
|
||||
return peripheral != null ? type + "_" + id : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public IPeripheral getPeripheral() {
|
||||
return peripheral;
|
||||
}
|
||||
|
||||
public boolean hasPeripheral() {
|
||||
return peripheral != null;
|
||||
}
|
||||
@ -100,9 +95,7 @@ public final class WiredModemLocalPeripheral {
|
||||
}
|
||||
|
||||
public Map<String, IPeripheral> toMap() {
|
||||
return peripheral == null
|
||||
? Map.of()
|
||||
: Collections.singletonMap(type + "_" + id, peripheral);
|
||||
return peripheral == null ? Map.of() : Map.of(type + "_" + id, peripheral);
|
||||
}
|
||||
|
||||
public void write(CompoundTag tag, String suffix) {
|
||||
|
@ -15,6 +15,7 @@ import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.api.peripheral.NotAttachedException;
|
||||
import dan200.computercraft.api.peripheral.WorkMonitor;
|
||||
import dan200.computercraft.core.apis.PeripheralAPI;
|
||||
import dan200.computercraft.core.computer.GuardedLuaContext;
|
||||
import dan200.computercraft.core.methods.PeripheralMethod;
|
||||
import dan200.computercraft.core.util.LuaUtil;
|
||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||
@ -22,6 +23,7 @@ import dan200.computercraft.shared.peripheral.modem.ModemPeripheral;
|
||||
import dan200.computercraft.shared.peripheral.modem.ModemState;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -34,12 +36,21 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WiredModemPeripheral.class);
|
||||
|
||||
private final WiredModemElement modem;
|
||||
private final WiredModemLocalPeripheral localPeripheral;
|
||||
private final BlockEntity target;
|
||||
|
||||
private final Map<IComputerAccess, ConcurrentMap<String, RemotePeripheralWrapper>> peripheralWrappers = new HashMap<>(1);
|
||||
|
||||
public WiredModemPeripheral(ModemState state, WiredModemElement modem) {
|
||||
public WiredModemPeripheral(
|
||||
ModemState state,
|
||||
WiredModemElement modem,
|
||||
WiredModemLocalPeripheral localPeripheral,
|
||||
BlockEntity target
|
||||
) {
|
||||
super(state);
|
||||
this.modem = modem;
|
||||
this.localPeripheral = localPeripheral;
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
//region IPacketSender implementation
|
||||
@ -62,8 +73,6 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
public Level getLevel() {
|
||||
return modem.getLevel();
|
||||
}
|
||||
|
||||
protected abstract WiredModemLocalPeripheral getLocalPeripheral();
|
||||
//endregion
|
||||
|
||||
@Override
|
||||
@ -207,7 +216,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
*/
|
||||
@LuaFunction
|
||||
public final @Nullable Object[] getNameLocal() {
|
||||
var local = getLocalPeripheral().getConnectedName();
|
||||
var local = localPeripheral.getConnectedName();
|
||||
return local == null ? null : new Object[]{ local };
|
||||
}
|
||||
|
||||
@ -218,8 +227,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
|
||||
ConcurrentMap<String, RemotePeripheralWrapper> wrappers;
|
||||
synchronized (peripheralWrappers) {
|
||||
wrappers = peripheralWrappers.get(computer);
|
||||
if (wrappers == null) peripheralWrappers.put(computer, wrappers = new ConcurrentHashMap<>());
|
||||
wrappers = peripheralWrappers.computeIfAbsent(computer, k -> new ConcurrentHashMap<>());
|
||||
}
|
||||
|
||||
synchronized (modem.getRemotePeripherals()) {
|
||||
@ -245,11 +253,13 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable IPeripheral other) {
|
||||
if (other instanceof WiredModemPeripheral otherModem) {
|
||||
return otherModem.modem == modem;
|
||||
}
|
||||
return false;
|
||||
public final boolean equals(@Nullable IPeripheral other) {
|
||||
return other instanceof WiredModemPeripheral otherModem && otherModem.modem == modem;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Object getTarget() {
|
||||
return target;
|
||||
}
|
||||
//endregion
|
||||
|
||||
@ -272,12 +282,11 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
var wrapper = wrappers.remove(name);
|
||||
if (wrapper != null) wrapper.detach();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void attachPeripheralImpl(IComputerAccess computer, ConcurrentMap<String, RemotePeripheralWrapper> peripherals, String periphName, IPeripheral peripheral) {
|
||||
if (!peripherals.containsKey(periphName) && !periphName.equals(getLocalPeripheral().getConnectedName())) {
|
||||
if (!peripherals.containsKey(periphName) && !periphName.equals(localPeripheral.getConnectedName())) {
|
||||
var methods = ServerContext.get(((ServerLevel) getLevel()).getServer()).peripheralMethods().getSelfMethods(peripheral);
|
||||
var wrapper = new RemotePeripheralWrapper(modem, peripheral, computer, periphName, methods);
|
||||
peripherals.put(periphName, wrapper);
|
||||
@ -296,7 +305,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
return wrappers == null ? null : wrappers.get(remoteName);
|
||||
}
|
||||
|
||||
private static class RemotePeripheralWrapper implements IComputerAccess {
|
||||
private static class RemotePeripheralWrapper implements IComputerAccess, GuardedLuaContext.Guard {
|
||||
private final WiredModemElement element;
|
||||
private final IPeripheral peripheral;
|
||||
private final IComputerAccess computer;
|
||||
@ -309,6 +318,8 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
private volatile boolean attached;
|
||||
private final Set<String> mounts = new HashSet<>();
|
||||
|
||||
private @Nullable GuardedLuaContext contextWrapper;
|
||||
|
||||
RemotePeripheralWrapper(WiredModemElement element, IPeripheral peripheral, IComputerAccess computer, String name, Map<String, PeripheralMethod> methods) {
|
||||
this.element = element;
|
||||
this.peripheral = peripheral;
|
||||
@ -356,7 +367,19 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
public MethodResult callMethod(ILuaContext context, String methodName, IArguments arguments) throws LuaException {
|
||||
var method = methodMap.get(methodName);
|
||||
if (method == null) throw new LuaException("No such method " + methodName);
|
||||
return method.apply(peripheral, context, this, arguments);
|
||||
|
||||
// Wrap the ILuaContext. We try to reuse the previous context where possible to avoid allocations.
|
||||
var contextWrapper = this.contextWrapper;
|
||||
if (contextWrapper == null || !contextWrapper.wraps(context)) {
|
||||
contextWrapper = this.contextWrapper = new GuardedLuaContext(context, this);
|
||||
}
|
||||
|
||||
return method.apply(peripheral, contextWrapper, this, arguments);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkValid() {
|
||||
return attached;
|
||||
}
|
||||
|
||||
// IComputerAccess implementation
|
||||
|
@ -44,13 +44,18 @@ public class MonitorBlockEntity extends BlockEntity {
|
||||
private final boolean advanced;
|
||||
|
||||
private @Nullable ServerMonitor serverMonitor;
|
||||
|
||||
/**
|
||||
* The monitor's state on the client. This is defined iff we're the origin monitor
|
||||
* ({@code xIndex == 0 && yIndex == 0}).
|
||||
*/
|
||||
private @Nullable ClientMonitor clientMonitor;
|
||||
|
||||
private @Nullable MonitorPeripheral peripheral;
|
||||
private final Set<IComputerAccess> computers = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
|
||||
private boolean needsUpdate = false;
|
||||
private boolean needsValidating = false;
|
||||
private boolean destroyed = false;
|
||||
|
||||
// MonitorWatcher state.
|
||||
boolean enqueued;
|
||||
@ -89,7 +94,7 @@ public class MonitorBlockEntity extends BlockEntity {
|
||||
@Override
|
||||
public void setRemoved() {
|
||||
super.setRemoved();
|
||||
if (clientMonitor != null && xIndex == 0 && yIndex == 0) clientMonitor.destroy();
|
||||
if (clientMonitor != null) clientMonitor.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -143,7 +148,7 @@ public class MonitorBlockEntity extends BlockEntity {
|
||||
private ServerMonitor getServerMonitor() {
|
||||
if (serverMonitor != null) return serverMonitor;
|
||||
|
||||
var origin = getOrigin().getMonitor();
|
||||
var origin = getOrigin();
|
||||
if (origin == null) return null;
|
||||
|
||||
return serverMonitor = origin.serverMonitor;
|
||||
@ -182,13 +187,11 @@ public class MonitorBlockEntity extends BlockEntity {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ClientMonitor getClientMonitor() {
|
||||
public ClientMonitor getOriginClientMonitor() {
|
||||
if (clientMonitor != null) return clientMonitor;
|
||||
|
||||
var te = level.getBlockEntity(toWorldPos(0, 0));
|
||||
if (!(te instanceof MonitorBlockEntity monitor)) return null;
|
||||
|
||||
return clientMonitor = monitor.clientMonitor;
|
||||
var origin = getOrigin();
|
||||
return origin == null ? null : origin.clientMonitor;
|
||||
}
|
||||
|
||||
// Networking stuff
|
||||
@ -209,17 +212,14 @@ public class MonitorBlockEntity extends BlockEntity {
|
||||
}
|
||||
|
||||
private void onClientLoad(int oldXIndex, int oldYIndex) {
|
||||
if (oldXIndex != xIndex || oldYIndex != yIndex) {
|
||||
// If our index has changed then it's possible the origin monitor has changed. Thus
|
||||
// we'll clear our cache. If we're the origin then we'll need to remove the glList as well.
|
||||
if (oldXIndex == 0 && oldYIndex == 0 && clientMonitor != null) clientMonitor.destroy();
|
||||
if ((oldXIndex != xIndex || oldYIndex != yIndex) && clientMonitor != null) {
|
||||
// If our index has changed, and we were the origin, then destroy the current monitor.
|
||||
clientMonitor.destroy();
|
||||
clientMonitor = null;
|
||||
}
|
||||
|
||||
if (xIndex == 0 && yIndex == 0) {
|
||||
// If we're the origin terminal then create it.
|
||||
if (clientMonitor == null) clientMonitor = new ClientMonitor(this);
|
||||
}
|
||||
// If we're the origin terminal then create it.
|
||||
if (xIndex == 0 && yIndex == 0 && clientMonitor == null) clientMonitor = new ClientMonitor(this);
|
||||
}
|
||||
|
||||
public final void read(TerminalState state) {
|
||||
@ -286,7 +286,7 @@ public class MonitorBlockEntity extends BlockEntity {
|
||||
}
|
||||
|
||||
boolean isCompatible(MonitorBlockEntity other) {
|
||||
return !other.destroyed && advanced == other.advanced && getOrientation() == other.getOrientation() && getDirection() == other.getDirection();
|
||||
return advanced == other.advanced && getOrientation() == other.getOrientation() && getDirection() == other.getDirection();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -309,8 +309,8 @@ public class MonitorBlockEntity extends BlockEntity {
|
||||
return isCompatible(monitor) ? MonitorState.present(monitor) : MonitorState.MISSING;
|
||||
}
|
||||
|
||||
private MonitorState getOrigin() {
|
||||
return getLoadedMonitor(0, 0);
|
||||
private @Nullable MonitorBlockEntity getOrigin() {
|
||||
return getLoadedMonitor(0, 0).getMonitor();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -389,7 +389,7 @@ public class MonitorBlockEntity extends BlockEntity {
|
||||
}
|
||||
|
||||
void expand() {
|
||||
var monitor = getOrigin().getMonitor();
|
||||
var monitor = getOrigin();
|
||||
if (monitor != null && monitor.xIndex == 0 && monitor.yIndex == 0) new Expander(monitor).expand();
|
||||
}
|
||||
|
||||
@ -558,7 +558,7 @@ public class MonitorBlockEntity extends BlockEntity {
|
||||
}
|
||||
|
||||
var hasPeripheral = false;
|
||||
var origin = getOrigin().getMonitor();
|
||||
var origin = getOrigin();
|
||||
var serverMonitor = origin != null ? origin.serverMonitor : this.serverMonitor;
|
||||
for (var x = 0; x < width; x++) {
|
||||
for (var y = 0; y < height; y++) {
|
||||
|
@ -37,7 +37,7 @@ class DfpwmState {
|
||||
private boolean unplayed = true;
|
||||
private long clientEndTime = PauseAwareTimer.getTime();
|
||||
private float pendingVolume = 1.0f;
|
||||
private @Nullable ByteBuffer pendingAudio;
|
||||
private @Nullable EncodedAudio pendingAudio;
|
||||
|
||||
synchronized boolean pushBuffer(LuaTable<?, ?> table, int size, Optional<Double> volume) throws LuaException {
|
||||
if (pendingAudio != null) return false;
|
||||
@ -45,6 +45,10 @@ class DfpwmState {
|
||||
var outSize = size / 8;
|
||||
var buffer = ByteBuffer.allocate(outSize);
|
||||
|
||||
var initialCharge = charge;
|
||||
var initialStrength = strength;
|
||||
var initialPreviousBit = previousBit;
|
||||
|
||||
for (var i = 0; i < outSize; i++) {
|
||||
var thisByte = 0;
|
||||
for (var j = 1; j <= 8; j++) {
|
||||
@ -80,7 +84,7 @@ class DfpwmState {
|
||||
|
||||
buffer.flip();
|
||||
|
||||
pendingAudio = buffer;
|
||||
pendingAudio = new EncodedAudio(initialCharge, initialStrength, initialPreviousBit, buffer);
|
||||
pendingVolume = (float) clampVolume(volume.orElse((double) pendingVolume));
|
||||
return true;
|
||||
}
|
||||
@ -89,12 +93,12 @@ class DfpwmState {
|
||||
return pendingAudio != null && now >= clientEndTime - CLIENT_BUFFER;
|
||||
}
|
||||
|
||||
ByteBuffer pullPending(long now) {
|
||||
EncodedAudio pullPending(long now) {
|
||||
var audio = pendingAudio;
|
||||
if (audio == null) throw new IllegalStateException("Should not pull pending audio yet");
|
||||
pendingAudio = null;
|
||||
// Compute when we should consider sending the next packet.
|
||||
clientEndTime = Math.max(now, clientEndTime) + (audio.remaining() * SECOND * 8 / SAMPLE_RATE);
|
||||
clientEndTime = Math.max(now, clientEndTime) + (audio.audio().remaining() * SECOND * 8 / SAMPLE_RATE);
|
||||
unplayed = false;
|
||||
return audio;
|
||||
}
|
||||
|
@ -0,0 +1,37 @@
|
||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.peripheral.speaker;
|
||||
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* A chunk of encoded audio, along with the state required for the decoder to reproduce the original audio samples.
|
||||
*
|
||||
* @param charge The DFPWM charge.
|
||||
* @param strength The DFPWM strength.
|
||||
* @param previousBit The previous bit.
|
||||
* @param audio The block of encoded audio.
|
||||
*/
|
||||
public record EncodedAudio(int charge, int strength, boolean previousBit, ByteBuffer audio) {
|
||||
public void write(FriendlyByteBuf buf) {
|
||||
buf.writeVarInt(charge());
|
||||
buf.writeVarInt(strength());
|
||||
buf.writeBoolean(previousBit());
|
||||
buf.writeBytes(audio().duplicate());
|
||||
}
|
||||
|
||||
public static EncodedAudio read(FriendlyByteBuf buf) {
|
||||
var charge = buf.readVarInt();
|
||||
var strength = buf.readVarInt();
|
||||
var previousBit = buf.readBoolean();
|
||||
|
||||
var bytes = new byte[buf.readableBytes()];
|
||||
buf.readBytes(bytes);
|
||||
|
||||
return new EncodedAudio(charge, strength, previousBit, ByteBuffer.wrap(bytes));
|
||||
}
|
||||
}
|
@ -375,4 +375,13 @@ public interface PlatformHelper extends dan200.computercraft.impl.PlatformHelper
|
||||
* @see ServerPlayerGameMode#useItemOn(ServerPlayer, Level, ItemStack, InteractionHand, BlockHitResult)
|
||||
*/
|
||||
InteractionResult useOn(ServerPlayer player, ItemStack stack, BlockHitResult hit, Predicate<BlockState> canUseBlock);
|
||||
|
||||
/**
|
||||
* Whether {@link net.minecraft.network.chat.ClickEvent.Action#RUN_COMMAND} can be used to run client commands.
|
||||
*
|
||||
* @return Whether client commands can be triggered from chat components.
|
||||
*/
|
||||
default boolean canClickRunClientCommand() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import dan200.computercraft.api.upgrades.UpgradeData;
|
||||
import dan200.computercraft.core.computer.ComputerSide;
|
||||
import dan200.computercraft.shared.common.IColouredItem;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ComputerState;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.config.Config;
|
||||
import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
|
||||
@ -38,7 +39,10 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
|
||||
private ItemStack stack = ItemStack.EMPTY;
|
||||
|
||||
private int lightColour = -1;
|
||||
private boolean lightChanged = false;
|
||||
|
||||
// The state the previous tick, used to determine if the state needs to be sent to the client.
|
||||
private int oldLightColour = -1;
|
||||
private @Nullable ComputerState oldComputerState;
|
||||
|
||||
private final Set<ServerPlayer> tracking = new HashSet<>();
|
||||
|
||||
@ -83,10 +87,7 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
|
||||
@Override
|
||||
public void setLight(int colour) {
|
||||
if (colour < 0 || colour > 0xFFFFFF) colour = -1;
|
||||
|
||||
if (lightColour == colour) return;
|
||||
lightColour = colour;
|
||||
lightChanged = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -151,9 +152,11 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
|
||||
tracking.removeIf(player -> !player.isAlive() || player.level() != getLevel());
|
||||
|
||||
// And now find any new players, add them to the tracking list, and broadcast state where appropriate.
|
||||
var sendState = hasOutputChanged() || lightChanged;
|
||||
lightChanged = false;
|
||||
if (sendState) {
|
||||
var state = getState();
|
||||
if (oldLightColour != lightColour || oldComputerState != state) {
|
||||
oldComputerState = state;
|
||||
oldLightColour = lightColour;
|
||||
|
||||
// Broadcast the state to all players
|
||||
tracking.addAll(getLevel().players());
|
||||
ServerNetworking.sendToPlayers(new PocketComputerDataMessage(this, false), tracking);
|
||||
@ -182,6 +185,6 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
|
||||
@Override
|
||||
protected void onRemoved() {
|
||||
super.onRemoved();
|
||||
ServerNetworking.sendToAllPlayers(new PocketComputerDeletedClientMessage(getInstanceID()), getLevel().getServer());
|
||||
ServerNetworking.sendToAllPlayers(new PocketComputerDeletedClientMessage(getInstanceUUID()), getLevel().getServer());
|
||||
}
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ import net.minecraft.world.level.Level;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public class PocketComputerItem extends Item implements IComputerItem, IMedia, IColouredItem {
|
||||
private static final String NBT_UPGRADE = "Upgrade";
|
||||
@ -188,10 +189,9 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
|
||||
}
|
||||
|
||||
public PocketServerComputer createServerComputer(ServerLevel level, Entity entity, @Nullable Container inventory, ItemStack stack) {
|
||||
var sessionID = getSessionID(stack);
|
||||
|
||||
var registry = ServerContext.get(level.getServer()).registry();
|
||||
var computer = (PocketServerComputer) registry.get(sessionID, getInstanceID(stack));
|
||||
var computer = (PocketServerComputer) registry.get(getSessionID(stack), getInstanceID(stack));
|
||||
if (computer == null) {
|
||||
var computerID = getComputerID(stack);
|
||||
if (computerID < 0) {
|
||||
@ -201,8 +201,9 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
|
||||
|
||||
computer = new PocketServerComputer(level, entity.blockPosition(), getComputerID(stack), getLabel(stack), getFamily());
|
||||
|
||||
setInstanceID(stack, computer.register());
|
||||
setSessionID(stack, registry.getSessionID());
|
||||
var tag = stack.getOrCreateTag();
|
||||
tag.putInt(NBT_SESSION, registry.getSessionID());
|
||||
tag.putUUID(NBT_INSTANCE, computer.register());
|
||||
|
||||
var upgrade = getUpgrade(stack);
|
||||
|
||||
@ -267,13 +268,9 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
|
||||
return null;
|
||||
}
|
||||
|
||||
public static int getInstanceID(ItemStack stack) {
|
||||
public static @Nullable UUID getInstanceID(ItemStack stack) {
|
||||
var nbt = stack.getTag();
|
||||
return nbt != null && nbt.contains(NBT_INSTANCE) ? nbt.getInt(NBT_INSTANCE) : -1;
|
||||
}
|
||||
|
||||
private static void setInstanceID(ItemStack stack, int instanceID) {
|
||||
stack.getOrCreateTag().putInt(NBT_INSTANCE, instanceID);
|
||||
return nbt != null && nbt.hasUUID(NBT_INSTANCE) ? nbt.getUUID(NBT_INSTANCE) : null;
|
||||
}
|
||||
|
||||
private static int getSessionID(ItemStack stack) {
|
||||
@ -281,10 +278,6 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
|
||||
return nbt != null && nbt.contains(NBT_SESSION) ? nbt.getInt(NBT_SESSION) : -1;
|
||||
}
|
||||
|
||||
private static void setSessionID(ItemStack stack, int sessionID) {
|
||||
stack.getOrCreateTag().putInt(NBT_SESSION, sessionID);
|
||||
}
|
||||
|
||||
private static boolean isMarkedOn(ItemStack stack) {
|
||||
var nbt = stack.getTag();
|
||||
return nbt != null && nbt.getBoolean(NBT_ON);
|
||||
|
@ -25,10 +25,9 @@ import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.ContainerHelper;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
@ -136,17 +135,8 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba
|
||||
super.loadServer(nbt);
|
||||
|
||||
// Read inventory
|
||||
var nbttaglist = nbt.getList("Items", Tag.TAG_COMPOUND);
|
||||
inventory.clear();
|
||||
inventorySnapshot.clear();
|
||||
for (var i = 0; i < nbttaglist.size(); i++) {
|
||||
var tag = nbttaglist.getCompound(i);
|
||||
var slot = tag.getByte("Slot") & 0xff;
|
||||
if (slot < getContainerSize()) {
|
||||
inventory.set(slot, ItemStack.of(tag));
|
||||
inventorySnapshot.set(slot, inventory.get(slot).copy());
|
||||
}
|
||||
}
|
||||
ContainerHelper.loadAllItems(nbt, inventory);
|
||||
for (var i = 0; i < inventory.size(); i++) inventorySnapshot.set(i, inventory.get(i).copy());
|
||||
|
||||
// Read state
|
||||
brain.readFromNBT(nbt);
|
||||
@ -155,16 +145,7 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba
|
||||
@Override
|
||||
public void saveAdditional(CompoundTag nbt) {
|
||||
// Write inventory
|
||||
var nbttaglist = new ListTag();
|
||||
for (var i = 0; i < INVENTORY_SIZE; i++) {
|
||||
if (!inventory.get(i).isEmpty()) {
|
||||
var tag = new CompoundTag();
|
||||
tag.putByte("Slot", (byte) i);
|
||||
inventory.get(i).save(tag);
|
||||
nbttaglist.add(tag);
|
||||
}
|
||||
}
|
||||
nbt.put("Items", nbttaglist);
|
||||
ContainerHelper.saveAllItems(nbt, inventory);
|
||||
|
||||
// Write brain
|
||||
nbt = brain.writeToNBT(nbt);
|
||||
@ -186,7 +167,7 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba
|
||||
if (dir.getAxis() == Direction.Axis.Y) dir = Direction.NORTH;
|
||||
level.setBlockAndUpdate(worldPosition, getBlockState().setValue(TurtleBlock.FACING, dir));
|
||||
|
||||
updateOutput();
|
||||
updateRedstone();
|
||||
updateInputsImmediately();
|
||||
|
||||
onTileEntityChange();
|
||||
|
@ -14,6 +14,7 @@ import dan200.computercraft.api.turtle.TurtleCommand;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.api.upgrades.UpgradeData;
|
||||
import dan200.computercraft.core.computer.ComputerSide;
|
||||
import dan200.computercraft.core.util.PeripheralHelpers;
|
||||
import dan200.computercraft.impl.TurtleUpgrades;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
@ -296,7 +297,7 @@ public class TurtleBrain implements TurtleAccessInternal {
|
||||
oldWorld.removeBlock(oldPos, false);
|
||||
|
||||
// Make sure everybody knows about it
|
||||
newTurtle.updateOutput();
|
||||
newTurtle.updateRedstone();
|
||||
newTurtle.updateInputsImmediately();
|
||||
return true;
|
||||
}
|
||||
@ -589,7 +590,7 @@ public class TurtleBrain implements TurtleAccessInternal {
|
||||
}
|
||||
|
||||
var existing = peripherals.get(side);
|
||||
if (existing == peripheral || (existing != null && peripheral != null && existing.equals(peripheral))) {
|
||||
if (PeripheralHelpers.equals(existing, peripheral)) {
|
||||
// If the peripheral is the same, just use that.
|
||||
peripheral = existing;
|
||||
} else {
|
||||
|
@ -54,7 +54,7 @@ public class TurtleSuckCommand implements TurtleCommand {
|
||||
case ContainerTransfer.NO_SPACE:
|
||||
return TurtleCommandResult.failure("No space for items");
|
||||
case ContainerTransfer.NO_ITEMS:
|
||||
return TurtleCommandResult.failure("No items to drop");
|
||||
return TurtleCommandResult.failure("No items to take");
|
||||
default:
|
||||
turtle.playAnimation(TurtleAnimation.WAIT);
|
||||
return TurtleCommandResult.success();
|
||||
|
@ -11,6 +11,11 @@ public final class DirectionUtil {
|
||||
private DirectionUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* A bitmask indicating all sides.
|
||||
*/
|
||||
public static final int ALL_SIDES = (1 << 6) - 1;
|
||||
|
||||
public static final Direction[] FACINGS = Direction.values();
|
||||
|
||||
public static ComputerSide toLocal(Direction front, Direction dir) {
|
||||
@ -31,4 +36,15 @@ public final class DirectionUtil {
|
||||
default -> 0.0f;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a direction is in a bitmask.
|
||||
*
|
||||
* @param mask The bitmask to test
|
||||
* @param direction The direction to check.
|
||||
* @return Whether the direction is in a bitmask.
|
||||
*/
|
||||
public static boolean isSet(int mask, Direction direction) {
|
||||
return (mask & (1 << direction.ordinal())) != 0;
|
||||
}
|
||||
}
|
||||
|
@ -5,13 +5,20 @@
|
||||
package dan200.computercraft.shared.util;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ChunkLevel;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelAccessor;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.chunk.ChunkStatus;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
|
||||
import java.util.Queue;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
|
||||
|
||||
/**
|
||||
* A thread-safe version of {@link LevelAccessor#scheduleTick(BlockPos, Block, int)}.
|
||||
@ -22,26 +29,92 @@ public final class TickScheduler {
|
||||
private TickScheduler() {
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of block entities to tick.
|
||||
*/
|
||||
private static final Queue<Token> toTick = new ConcurrentLinkedDeque<>();
|
||||
|
||||
/**
|
||||
* Block entities which we want to tick, but whose chunks not currently loaded.
|
||||
* <p>
|
||||
* Minecraft sometimes keeps chunks in-memory, but not actively loaded. If such a block entity is in the
|
||||
* {@link #toTick} queue, we'll see that it's not loaded and so have to skip scheduling a tick.
|
||||
* <p>
|
||||
* However, if the block entity is ever loaded again, we need to tick it. Unfortunately, block entities in this
|
||||
* state are not notified in any way (for instance, {@link BlockEntity#setRemoved()} or
|
||||
* {@link BlockEntity#clearRemoved()} are not called), and so there's no way to easily reschedule them for ticking.
|
||||
* <p>
|
||||
* Instead, for each chunk we keep a list of all block entities whose tick we skipped. If a chunk is loaded,
|
||||
* {@linkplain #onChunkTicketChanged(ServerLevel, long, int, int) we requeue all skipped ticks}.
|
||||
*/
|
||||
private static final Map<ChunkReference, List<Token>> delayed = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Schedule a block entity to be ticked.
|
||||
*
|
||||
* @param token The token whose block entity should be ticked.
|
||||
*/
|
||||
public static void schedule(Token token) {
|
||||
var world = token.owner.getLevel();
|
||||
if (world != null && !world.isClientSide && !token.scheduled.getAndSet(true)) toTick.add(token);
|
||||
if (world != null && !world.isClientSide && Token.STATE.compareAndSet(token, State.IDLE, State.SCHEDULED)) {
|
||||
toTick.add(token);
|
||||
}
|
||||
}
|
||||
|
||||
public static void onChunkTicketChanged(ServerLevel level, long chunkPos, int oldLevel, int newLevel) {
|
||||
boolean oldLoaded = isLoaded(oldLevel), newLoaded = isLoaded(newLevel);
|
||||
if (!oldLoaded && newLoaded) {
|
||||
// If our chunk is becoming active, requeue all pending tokens.
|
||||
var delayedTokens = delayed.remove(new ChunkReference(level.dimension(), chunkPos));
|
||||
if (delayedTokens == null) return;
|
||||
|
||||
for (var token : delayedTokens) {
|
||||
if (token.owner.isRemoved()) {
|
||||
Token.STATE.set(token, State.IDLE);
|
||||
} else {
|
||||
Token.STATE.set(token, State.SCHEDULED);
|
||||
toTick.add(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void onChunkUnload(LevelChunk chunk) {
|
||||
// If our chunk is fully unloaded, all block entities are about to be removed - we need to dequeue any delayed
|
||||
// tokens from the queue.
|
||||
var delayedTokens = delayed.remove(new ChunkReference(chunk.getLevel().dimension(), chunk.getPos().toLong()));
|
||||
if (delayedTokens == null) return;
|
||||
|
||||
for (var token : delayedTokens) Token.STATE.set(token, State.IDLE);
|
||||
}
|
||||
|
||||
public static void tick() {
|
||||
Token token;
|
||||
while ((token = toTick.poll()) != null) {
|
||||
token.scheduled.set(false);
|
||||
var blockEntity = token.owner;
|
||||
if (blockEntity.isRemoved()) continue;
|
||||
while ((token = toTick.poll()) != null) Token.STATE.set(token, tickToken(token));
|
||||
}
|
||||
|
||||
var world = blockEntity.getLevel();
|
||||
var pos = blockEntity.getBlockPos();
|
||||
private static State tickToken(Token token) {
|
||||
var blockEntity = token.owner;
|
||||
|
||||
if (world != null && world.isLoaded(pos) && world.getBlockEntity(pos) == blockEntity) {
|
||||
world.scheduleTick(pos, blockEntity.getBlockState().getBlock(), 0);
|
||||
// If the block entity has been removed, then remove it from the queue.
|
||||
if (blockEntity.isRemoved()) return State.IDLE;
|
||||
|
||||
var level = Objects.requireNonNull(blockEntity.getLevel(), "Block entity level cannot become null");
|
||||
var pos = blockEntity.getBlockPos();
|
||||
|
||||
if (!level.isLoaded(pos)) {
|
||||
// The chunk is not properly loaded, as it to our delayed set.
|
||||
delayed.computeIfAbsent(new ChunkReference(level.dimension(), ChunkPos.asLong(pos)), x -> new ArrayList<>()).add(token);
|
||||
return State.UNLOADED;
|
||||
} else {
|
||||
// This should be impossible: either the block entity is at the above position, or it has been removed.
|
||||
if (level.getBlockEntity(pos) != blockEntity) {
|
||||
throw new IllegalStateException("Expected " + blockEntity + " at " + pos);
|
||||
}
|
||||
|
||||
// Otherwise schedule a tick and remove it from the queue.
|
||||
level.scheduleTick(pos, blockEntity.getBlockState().getBlock(), 0);
|
||||
return State.IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,11 +125,51 @@ public final class TickScheduler {
|
||||
* As such, it should be unique per {@link BlockEntity} instance to avoid it being queued multiple times.
|
||||
*/
|
||||
public static class Token {
|
||||
static final AtomicReferenceFieldUpdater<Token, State> STATE = AtomicReferenceFieldUpdater.newUpdater(Token.class, State.class, "$state");
|
||||
|
||||
final BlockEntity owner;
|
||||
final AtomicBoolean scheduled = new AtomicBoolean();
|
||||
|
||||
/**
|
||||
* The current state of this token.
|
||||
*/
|
||||
private volatile State $state = State.IDLE;
|
||||
|
||||
public Token(BlockEntity owner) {
|
||||
this.owner = owner;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The possible states a {@link Token} can be in.
|
||||
* <p>
|
||||
* This effectively stores which (if any) queue the token is currently in, allowing us to skip scheduling if the
|
||||
* token is already enqueued.
|
||||
*/
|
||||
private enum State {
|
||||
/**
|
||||
* The token is not on any queues.
|
||||
*/
|
||||
IDLE,
|
||||
|
||||
/**
|
||||
* The token is on the {@link #toTick} queue.
|
||||
*/
|
||||
SCHEDULED,
|
||||
|
||||
/**
|
||||
* The token is on the {@link #delayed} queue.
|
||||
*/
|
||||
UNLOADED,
|
||||
}
|
||||
|
||||
private record ChunkReference(ResourceKey<Level> level, Long position) {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChunkReference(" + level + " at " + new ChunkPos(position) + ")";
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isLoaded(int level) {
|
||||
return level <= ChunkLevel.byStatus(ChunkStatus.FULL);
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ accessWidener v1 named
|
||||
# Additional access wideners for vanilla code. This is a effectively the subset of Fabric's transitive access wideners
|
||||
# that we actually use
|
||||
|
||||
accessible method net/minecraft/client/renderer/item/ItemProperties register (Lnet/minecraft/world/item/Item;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/renderer/item/ClampedItemPropertyFunction;)V
|
||||
accessible method net/minecraft/client/renderer/blockentity/BlockEntityRenderers register (Lnet/minecraft/world/level/block/entity/BlockEntityType;Lnet/minecraft/client/renderer/blockentity/BlockEntityRendererProvider;)V
|
||||
accessible class net/minecraft/world/item/CreativeModeTab$Output
|
||||
accessible field net/minecraft/world/item/CreativeModeTabs OP_BLOCKS Lnet/minecraft/resources/ResourceKey;
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
package dan200.computercraft.client.sound;
|
||||
|
||||
import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
@ -16,7 +17,7 @@ public class DfpwmStreamTest {
|
||||
var stream = new DfpwmStream();
|
||||
|
||||
var input = ByteBuffer.wrap(new byte[]{ 43, -31, 33, 44, 30, -16, -85, 23, -3, -55, 46, -70, 68, -67, 74, -96, -68, 16, 94, -87, -5, 87, 11, -16, 19, 92, 85, -71, 126, 5, -84, 64, 17, -6, 85, -11, -1, -87, -12, 1, 85, -56, 33, -80, 82, 104, -93, 17, 126, 23, 91, -30, 37, -32, 117, -72, -58, 11, -76, 19, -108, 86, -65, -10, -1, -68, -25, 10, -46, 85, 124, -54, 15, -24, 43, -94, 117, 63, -36, 15, -6, 88, 87, -26, -83, 106, 41, 13, -28, -113, -10, -66, 119, -87, -113, 68, -55, 40, -107, 62, 20, 72, 3, -96, 114, -87, -2, 39, -104, 30, 20, 42, 84, 24, 47, 64, 43, 61, -35, 95, -65, 42, 61, 42, -50, 4, -9, 81 });
|
||||
stream.push(input);
|
||||
stream.push(new EncodedAudio(0, 0, false, input));
|
||||
|
||||
var buffer = stream.read(1024 + 1);
|
||||
assertEquals(1024, buffer.remaining(), "Must have read 1024 bytes");
|
||||
|
@ -0,0 +1,183 @@
|
||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.impl.network.wired;
|
||||
|
||||
import dan200.computercraft.api.network.wired.WiredNetwork;
|
||||
import dan200.computercraft.impl.network.wired.NetworkTest.NetworkElement;
|
||||
import dan200.computercraft.shared.util.DirectionUtil;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import org.openjdk.jmh.annotations.*;
|
||||
import org.openjdk.jmh.runner.Runner;
|
||||
import org.openjdk.jmh.runner.RunnerException;
|
||||
import org.openjdk.jmh.runner.options.OptionsBuilder;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
|
||||
@BenchmarkMode(Mode.AverageTime)
|
||||
@OutputTimeUnit(TimeUnit.MILLISECONDS)
|
||||
public class NetworkBenchmark {
|
||||
private static final int BRUTE_SIZE = 16;
|
||||
|
||||
public static void main(String[] args) throws RunnerException {
|
||||
var opts = new OptionsBuilder()
|
||||
.include(NetworkBenchmark.class.getName() + "\\..*")
|
||||
.warmupIterations(2)
|
||||
.measurementIterations(5)
|
||||
.forks(1)
|
||||
.build();
|
||||
new Runner(opts).run();
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
@Warmup(time = 1, timeUnit = TimeUnit.SECONDS)
|
||||
@Measurement(time = 2, timeUnit = TimeUnit.SECONDS)
|
||||
public void removeEveryNode(ConnectedGrid grid) {
|
||||
grid.grid.forEach((node, pos) -> node.remove());
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
@Warmup(time = 1, timeUnit = TimeUnit.SECONDS)
|
||||
@Measurement(time = 2, timeUnit = TimeUnit.SECONDS)
|
||||
public void connectAndDisconnect(SplitGrid connectedGrid) {
|
||||
WiredNodeImpl left = connectedGrid.left, right = connectedGrid.right;
|
||||
|
||||
assertNotEquals(left.getNetwork(), right.getNetwork());
|
||||
left.connectTo(right);
|
||||
assertEquals(left.getNetwork(), right.getNetwork());
|
||||
left.disconnectFrom(right);
|
||||
assertNotEquals(left.getNetwork(), right.getNetwork());
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
@Warmup(time = 1, timeUnit = TimeUnit.SECONDS)
|
||||
@Measurement(time = 2, timeUnit = TimeUnit.SECONDS)
|
||||
public void connectAndRemove(SplitGrid connectedGrid) {
|
||||
WiredNodeImpl left = connectedGrid.left, right = connectedGrid.right, centre = connectedGrid.centre;
|
||||
|
||||
assertNotEquals(left.getNetwork(), right.getNetwork());
|
||||
centre.connectTo(left);
|
||||
centre.connectTo(right);
|
||||
assertEquals(left.getNetwork(), right.getNetwork());
|
||||
centre.remove();
|
||||
assertNotEquals(left.getNetwork(), right.getNetwork());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a grid where all nodes are connected to their neighbours.
|
||||
*/
|
||||
@State(Scope.Thread)
|
||||
public static class ConnectedGrid {
|
||||
Grid<WiredNodeImpl> grid;
|
||||
|
||||
@Setup(Level.Invocation)
|
||||
public void setup() {
|
||||
var grid = this.grid = new Grid<>(BRUTE_SIZE);
|
||||
grid.map((existing, pos) -> new NetworkElement("n_" + pos, pos.getX() == pos.getY() && pos.getY() == pos.getZ()).getNode());
|
||||
|
||||
// Connect every node
|
||||
grid.forEach((node, pos) -> {
|
||||
for (var facing : DirectionUtil.FACINGS) {
|
||||
var other = grid.get(pos.relative(facing));
|
||||
if (other != null) node.connectTo(other);
|
||||
}
|
||||
});
|
||||
|
||||
var networks = countNetworks(grid);
|
||||
if (networks.size() != 1) throw new AssertionError("Expected exactly one network.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a grid where the nodes at {@code x < BRUTE_SIZE/2} and {@code x >= BRUTE_SIZE/2} are in separate networks,
|
||||
* but otherwise connected to their neighbours.
|
||||
*/
|
||||
@State(Scope.Thread)
|
||||
public static class SplitGrid {
|
||||
Grid<WiredNodeImpl> grid;
|
||||
WiredNodeImpl left, right, centre;
|
||||
|
||||
@Setup
|
||||
public void setup() {
|
||||
var grid = this.grid = new Grid<>(BRUTE_SIZE);
|
||||
grid.map((existing, pos) -> new NetworkElement("n_" + pos, pos.getX() == pos.getY() && pos.getY() == pos.getZ()).getNode());
|
||||
|
||||
// Connect every node
|
||||
grid.forEach((node, pos) -> {
|
||||
for (var facing : DirectionUtil.FACINGS) {
|
||||
var offset = pos.relative(facing);
|
||||
if (offset.getX() >= BRUTE_SIZE / 2 == pos.getX() >= BRUTE_SIZE / 2) {
|
||||
var other = grid.get(offset);
|
||||
if (other != null) node.connectTo(other);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var networks = countNetworks(grid);
|
||||
if (networks.size() != 2) throw new AssertionError("Expected exactly two networks.");
|
||||
for (var network : networks.object2IntEntrySet()) {
|
||||
if (network.getIntValue() != BRUTE_SIZE * BRUTE_SIZE * (BRUTE_SIZE / 2)) {
|
||||
throw new AssertionError("Network is the wrong size");
|
||||
}
|
||||
}
|
||||
|
||||
left = Objects.requireNonNull(grid.get(new BlockPos(BRUTE_SIZE / 2 - 1, 0, 0)));
|
||||
right = Objects.requireNonNull(grid.get(new BlockPos(BRUTE_SIZE / 2, 0, 0)));
|
||||
centre = new NetworkElement("c", false).getNode();
|
||||
}
|
||||
}
|
||||
|
||||
private static Object2IntMap<WiredNetwork> countNetworks(Grid<WiredNodeImpl> grid) {
|
||||
Object2IntMap<WiredNetwork> networks = new Object2IntOpenHashMap<>();
|
||||
grid.forEach((node, pos) -> networks.put(node.network, networks.getOrDefault(node.network, 0) + 1));
|
||||
return networks;
|
||||
}
|
||||
|
||||
private static class Grid<T> {
|
||||
private final int size;
|
||||
private final T[] box;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Grid(int size) {
|
||||
this.size = size;
|
||||
this.box = (T[]) new Object[size * size * size];
|
||||
}
|
||||
|
||||
public T get(BlockPos pos) {
|
||||
int x = pos.getX(), y = pos.getY(), z = pos.getZ();
|
||||
|
||||
return x >= 0 && x < size && y >= 0 && y < size && z >= 0 && z < size
|
||||
? box[x * size * size + y * size + z]
|
||||
: null;
|
||||
}
|
||||
|
||||
public void forEach(BiConsumer<T, BlockPos> transform) {
|
||||
for (var x = 0; x < size; x++) {
|
||||
for (var y = 0; y < size; y++) {
|
||||
for (var z = 0; z < size; z++) {
|
||||
transform.accept(box[x * size * size + y * size + z], new BlockPos(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void map(BiFunction<T, BlockPos, T> transform) {
|
||||
for (var x = 0; x < size; x++) {
|
||||
for (var y = 0; y < size; y++) {
|
||||
for (var z = 0; z < size; z++) {
|
||||
box[x * size * size + y * size + z] = transform.apply(box[x * size * size + y * size + z], new BlockPos(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -9,19 +9,14 @@ import dan200.computercraft.api.network.wired.WiredNetwork;
|
||||
import dan200.computercraft.api.network.wired.WiredNetworkChange;
|
||||
import dan200.computercraft.api.network.wired.WiredNode;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.shared.util.DirectionUtil;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
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;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@ -29,11 +24,11 @@ public class NetworkTest {
|
||||
@Test
|
||||
public void testConnect() {
|
||||
NetworkElement
|
||||
aE = new NetworkElement(null, null, "a"),
|
||||
bE = new NetworkElement(null, null, "b"),
|
||||
cE = new NetworkElement(null, null, "c");
|
||||
aE = new NetworkElement("a"),
|
||||
bE = new NetworkElement("b"),
|
||||
cE = new NetworkElement("c");
|
||||
|
||||
WiredNode
|
||||
WiredNodeImpl
|
||||
aN = aE.getNode(),
|
||||
bN = bE.getNode(),
|
||||
cN = cE.getNode();
|
||||
@ -42,8 +37,8 @@ public class NetworkTest {
|
||||
assertNotEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be different");
|
||||
assertNotEquals(bN.getNetwork(), cN.getNetwork(), "B's and C's network must be different");
|
||||
|
||||
assertTrue(aN.getNetwork().connect(aN, bN), "Must be able to add connection");
|
||||
assertFalse(aN.getNetwork().connect(aN, bN), "Cannot add connection twice");
|
||||
assertTrue(aN.connectTo(bN), "Must be able to add connection");
|
||||
assertFalse(aN.connectTo(bN), "Cannot add connection twice");
|
||||
|
||||
assertEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must be equal");
|
||||
assertEquals(Set.of(aN, bN), nodes(aN.getNetwork()), "A's network should be A and B");
|
||||
@ -51,7 +46,7 @@ public class NetworkTest {
|
||||
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);
|
||||
aN.connectTo(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");
|
||||
@ -69,20 +64,20 @@ public class NetworkTest {
|
||||
@Test
|
||||
public void testDisconnectNoChange() {
|
||||
NetworkElement
|
||||
aE = new NetworkElement(null, null, "a"),
|
||||
bE = new NetworkElement(null, null, "b"),
|
||||
cE = new NetworkElement(null, null, "c");
|
||||
aE = new NetworkElement("a"),
|
||||
bE = new NetworkElement("b"),
|
||||
cE = new NetworkElement("c");
|
||||
|
||||
WiredNode
|
||||
WiredNodeImpl
|
||||
aN = aE.getNode(),
|
||||
bN = bE.getNode(),
|
||||
cN = cE.getNode();
|
||||
|
||||
aN.getNetwork().connect(aN, bN);
|
||||
aN.getNetwork().connect(aN, cN);
|
||||
aN.getNetwork().connect(bN, cN);
|
||||
aN.connectTo(bN);
|
||||
aN.connectTo(cN);
|
||||
bN.connectTo(cN);
|
||||
|
||||
aN.getNetwork().disconnect(aN, bN);
|
||||
aN.disconnectFrom(bN);
|
||||
|
||||
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");
|
||||
@ -96,19 +91,19 @@ public class NetworkTest {
|
||||
@Test
|
||||
public void testDisconnectLeaf() {
|
||||
NetworkElement
|
||||
aE = new NetworkElement(null, null, "a"),
|
||||
bE = new NetworkElement(null, null, "b"),
|
||||
cE = new NetworkElement(null, null, "c");
|
||||
aE = new NetworkElement("a"),
|
||||
bE = new NetworkElement("b"),
|
||||
cE = new NetworkElement("c");
|
||||
|
||||
WiredNode
|
||||
WiredNodeImpl
|
||||
aN = aE.getNode(),
|
||||
bN = bE.getNode(),
|
||||
cN = cE.getNode();
|
||||
|
||||
aN.getNetwork().connect(aN, bN);
|
||||
aN.getNetwork().connect(aN, cN);
|
||||
aN.connectTo(bN);
|
||||
aN.connectTo(cN);
|
||||
|
||||
aN.getNetwork().disconnect(aN, bN);
|
||||
aN.disconnectFrom(bN);
|
||||
|
||||
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");
|
||||
@ -123,23 +118,23 @@ public class NetworkTest {
|
||||
@Test
|
||||
public void testDisconnectSplit() {
|
||||
NetworkElement
|
||||
aE = new NetworkElement(null, null, "a"),
|
||||
aaE = new NetworkElement(null, null, "a_"),
|
||||
bE = new NetworkElement(null, null, "b"),
|
||||
bbE = new NetworkElement(null, null, "b_");
|
||||
aE = new NetworkElement("a"),
|
||||
aaE = new NetworkElement("a_"),
|
||||
bE = new NetworkElement("b"),
|
||||
bbE = new NetworkElement("b_");
|
||||
|
||||
WiredNode
|
||||
WiredNodeImpl
|
||||
aN = aE.getNode(),
|
||||
aaN = aaE.getNode(),
|
||||
bN = bE.getNode(),
|
||||
bbN = bbE.getNode();
|
||||
|
||||
aN.getNetwork().connect(aN, aaN);
|
||||
bN.getNetwork().connect(bN, bbN);
|
||||
aN.connectTo(aaN);
|
||||
bN.connectTo(bbN);
|
||||
|
||||
aN.getNetwork().connect(aN, bN);
|
||||
aN.connectTo(bN);
|
||||
|
||||
aN.getNetwork().disconnect(aN, bN);
|
||||
aN.disconnectFrom(bN);
|
||||
|
||||
assertNotEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal");
|
||||
assertEquals(aN.getNetwork(), aaN.getNetwork(), "A's and A_'s network must be equal");
|
||||
@ -154,7 +149,7 @@ public class NetworkTest {
|
||||
|
||||
@Test
|
||||
public void testRemoveSingle() {
|
||||
var aE = new NetworkElement(null, null, "a");
|
||||
var aE = new NetworkElement("a");
|
||||
var aN = aE.getNode();
|
||||
|
||||
var network = aN.getNetwork();
|
||||
@ -165,20 +160,20 @@ public class NetworkTest {
|
||||
@Test
|
||||
public void testRemoveLeaf() {
|
||||
NetworkElement
|
||||
aE = new NetworkElement(null, null, "a"),
|
||||
bE = new NetworkElement(null, null, "b"),
|
||||
cE = new NetworkElement(null, null, "c");
|
||||
aE = new NetworkElement("a"),
|
||||
bE = new NetworkElement("b"),
|
||||
cE = new NetworkElement("c");
|
||||
|
||||
WiredNode
|
||||
WiredNodeImpl
|
||||
aN = aE.getNode(),
|
||||
bN = bE.getNode(),
|
||||
cN = cE.getNode();
|
||||
|
||||
aN.getNetwork().connect(aN, bN);
|
||||
aN.getNetwork().connect(aN, cN);
|
||||
aN.connectTo(bN);
|
||||
aN.connectTo(cN);
|
||||
|
||||
assertTrue(aN.getNetwork().remove(bN), "Must be able to remove node");
|
||||
assertFalse(aN.getNetwork().remove(bN), "Cannot remove a second time");
|
||||
assertTrue(bN.remove(), "Must be able to remove node");
|
||||
assertFalse(bN.remove(), "Cannot remove a second time");
|
||||
|
||||
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");
|
||||
@ -194,26 +189,26 @@ public class NetworkTest {
|
||||
@Test
|
||||
public void testRemoveSplit() {
|
||||
NetworkElement
|
||||
aE = new NetworkElement(null, null, "a"),
|
||||
aaE = new NetworkElement(null, null, "a_"),
|
||||
bE = new NetworkElement(null, null, "b"),
|
||||
bbE = new NetworkElement(null, null, "b_"),
|
||||
cE = new NetworkElement(null, null, "c");
|
||||
aE = new NetworkElement("a"),
|
||||
aaE = new NetworkElement("a_"),
|
||||
bE = new NetworkElement("b"),
|
||||
bbE = new NetworkElement("b_"),
|
||||
cE = new NetworkElement("c");
|
||||
|
||||
WiredNode
|
||||
WiredNodeImpl
|
||||
aN = aE.getNode(),
|
||||
aaN = aaE.getNode(),
|
||||
bN = bE.getNode(),
|
||||
bbN = bbE.getNode(),
|
||||
cN = cE.getNode();
|
||||
|
||||
aN.getNetwork().connect(aN, aaN);
|
||||
bN.getNetwork().connect(bN, bbN);
|
||||
aN.connectTo(aaN);
|
||||
bN.connectTo(bbN);
|
||||
|
||||
cN.getNetwork().connect(aN, cN);
|
||||
cN.getNetwork().connect(bN, cN);
|
||||
cN.connectTo(aN);
|
||||
cN.connectTo(bN);
|
||||
|
||||
cN.getNetwork().remove(cN);
|
||||
cN.remove();
|
||||
|
||||
assertNotEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal");
|
||||
assertEquals(aN.getNetwork(), aaN.getNetwork(), "A's and A_'s network must be equal");
|
||||
@ -228,96 +223,30 @@ public class NetworkTest {
|
||||
assertEquals(Set.of(), cE.allPeripherals().keySet(), "C's peripheral set should be empty");
|
||||
}
|
||||
|
||||
private static final int BRUTE_SIZE = 16;
|
||||
private static final int TOGGLE_CONNECTION_TIMES = 5;
|
||||
private static final int TOGGLE_NODE_TIMES = 5;
|
||||
|
||||
@Test
|
||||
@Disabled("Takes a long time to run, mostly for stress testing")
|
||||
public void testLarge() {
|
||||
var grid = new Grid<WiredNode>(BRUTE_SIZE);
|
||||
grid.map((existing, pos) -> new NetworkElement(null, null, "n_" + pos).getNode());
|
||||
|
||||
// Test connecting
|
||||
{
|
||||
var start = System.nanoTime();
|
||||
|
||||
grid.forEach((existing, pos) -> {
|
||||
for (var facing : DirectionUtil.FACINGS) {
|
||||
var offset = pos.relative(facing);
|
||||
if (offset.getX() > BRUTE_SIZE / 2 == pos.getX() > BRUTE_SIZE / 2) {
|
||||
var other = grid.get(offset);
|
||||
if (other != null) existing.getNetwork().connect(existing, other);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var end = System.nanoTime();
|
||||
|
||||
System.out.printf("Connecting %s³ nodes took %s seconds\n", BRUTE_SIZE, (end - start) * 1e-9);
|
||||
}
|
||||
|
||||
// Test toggling
|
||||
{
|
||||
var left = grid.get(new BlockPos(BRUTE_SIZE / 2, 0, 0));
|
||||
var right = grid.get(new BlockPos(BRUTE_SIZE / 2 + 1, 0, 0));
|
||||
assertNotEquals(left.getNetwork(), right.getNetwork());
|
||||
|
||||
var start = System.nanoTime();
|
||||
for (var i = 0; i < TOGGLE_CONNECTION_TIMES; i++) {
|
||||
left.getNetwork().connect(left, right);
|
||||
left.getNetwork().disconnect(left, right);
|
||||
}
|
||||
|
||||
var end = System.nanoTime();
|
||||
|
||||
System.out.printf("Toggling connection %s times took %s seconds\n", TOGGLE_CONNECTION_TIMES, (end - start) * 1e-9);
|
||||
}
|
||||
|
||||
{
|
||||
var left = grid.get(new BlockPos(BRUTE_SIZE / 2, 0, 0));
|
||||
var right = grid.get(new BlockPos(BRUTE_SIZE / 2 + 1, 0, 0));
|
||||
var centre = new NetworkElement(null, null, "c").getNode();
|
||||
assertNotEquals(left.getNetwork(), right.getNetwork());
|
||||
|
||||
var start = System.nanoTime();
|
||||
for (var i = 0; i < TOGGLE_NODE_TIMES; i++) {
|
||||
left.getNetwork().connect(left, centre);
|
||||
right.getNetwork().connect(right, centre);
|
||||
|
||||
left.getNetwork().remove(centre);
|
||||
}
|
||||
|
||||
var end = System.nanoTime();
|
||||
|
||||
System.out.printf("Toggling node %s times took %s seconds\n", TOGGLE_NODE_TIMES, (end - start) * 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class NetworkElement implements WiredElement {
|
||||
private final Level world;
|
||||
private final Vec3 position;
|
||||
static final class NetworkElement implements WiredElement {
|
||||
private final String id;
|
||||
private final WiredNode node;
|
||||
private final WiredNodeImpl node;
|
||||
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;
|
||||
this.position = position;
|
||||
NetworkElement(String id) {
|
||||
this(id, true);
|
||||
}
|
||||
|
||||
NetworkElement(String id, boolean peripheral) {
|
||||
this.id = id;
|
||||
this.node = new WiredNodeImpl(this);
|
||||
this.addPeripheral(id);
|
||||
if (peripheral) addPeripheral(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Level getLevel() {
|
||||
return world;
|
||||
throw new IllegalStateException("Unexpected call to getLevel()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 getPosition() {
|
||||
return position;
|
||||
throw new IllegalStateException("Unexpected call to getPosition()");
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -331,7 +260,7 @@ public class NetworkTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public WiredNode getNode() {
|
||||
public WiredNodeImpl getNode() {
|
||||
return node;
|
||||
}
|
||||
|
||||
@ -364,45 +293,6 @@ public class NetworkTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static class Grid<T> {
|
||||
private final int size;
|
||||
private final T[] box;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Grid(int size) {
|
||||
this.size = size;
|
||||
this.box = (T[]) new Object[size * size * size];
|
||||
}
|
||||
|
||||
public T get(BlockPos pos) {
|
||||
int x = pos.getX(), y = pos.getY(), z = pos.getZ();
|
||||
|
||||
return x >= 0 && x < size && y >= 0 && y < size && z >= 0 && z < size
|
||||
? box[x * size * size + y * size + z]
|
||||
: null;
|
||||
}
|
||||
|
||||
public void forEach(BiConsumer<T, BlockPos> transform) {
|
||||
for (var x = 0; x < size; x++) {
|
||||
for (var y = 0; y < size; y++) {
|
||||
for (var z = 0; z < size; z++) {
|
||||
transform.accept(box[x * size * size + y * size + z], new BlockPos(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void map(BiFunction<T, BlockPos, T> transform) {
|
||||
for (var x = 0; x < size; x++) {
|
||||
for (var y = 0; y < size; y++) {
|
||||
for (var z = 0; z < size; z++) {
|
||||
box[x * size * size + y * size + z] = transform.apply(box[x * size * size + y * size + z], new BlockPos(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<WiredNodeImpl> nodes(WiredNetwork network) {
|
||||
return ((WiredNetworkImpl) network).nodes;
|
||||
}
|
||||
|
@ -0,0 +1,91 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.command.arguments;
|
||||
|
||||
import com.mojang.brigadier.StringReader;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.context.StringRange;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.Suggestion;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import dan200.computercraft.shared.command.Exceptions;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.test.core.ReplaceUnderscoresDisplayNameGenerator;
|
||||
import org.junit.jupiter.api.DisplayNameGeneration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
@DisplayNameGeneration(ReplaceUnderscoresDisplayNameGenerator.class)
|
||||
class ComputerSelectorTest {
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@MethodSource("getArgumentTestCases")
|
||||
public void Parse_basic_inputs(String input, ComputerSelector expected) throws CommandSyntaxException {
|
||||
assertEquals(expected, ComputerSelector.parse(new StringReader(input)));
|
||||
}
|
||||
|
||||
public static Arguments[] getArgumentTestCases() {
|
||||
return new Arguments[]{
|
||||
// Legacy selectors
|
||||
Arguments.of("@some_label", new ComputerSelector("@some_label", OptionalInt.empty(), null, OptionalInt.empty(), "some_label", null, null, null)),
|
||||
Arguments.of("~normal", new ComputerSelector("~normal", OptionalInt.empty(), null, OptionalInt.empty(), null, ComputerFamily.NORMAL, null, null)),
|
||||
Arguments.of("#123", new ComputerSelector("#123", OptionalInt.empty(), null, OptionalInt.of(123), null, null, null, null)),
|
||||
Arguments.of("123", new ComputerSelector("123", OptionalInt.of(123), null, OptionalInt.empty(), null, null, null, null)),
|
||||
// New selectors
|
||||
Arguments.of("@c[]", new ComputerSelector("@c[]", OptionalInt.empty(), null, OptionalInt.empty(), null, null, null, null)),
|
||||
Arguments.of("@c[instance=5e18f505-62f7-46f8-83f3-792f03224724]", new ComputerSelector("@c[instance=5e18f505-62f7-46f8-83f3-792f03224724]", OptionalInt.empty(), UUID.fromString("5e18f505-62f7-46f8-83f3-792f03224724"), OptionalInt.empty(), null, null, null, null)),
|
||||
Arguments.of("@c[id=123]", new ComputerSelector("@c[id=123]", OptionalInt.empty(), null, OptionalInt.of(123), null, null, null, null)),
|
||||
Arguments.of("@c[label=\"foo\"]", new ComputerSelector("@c[label=\"foo\"]", OptionalInt.empty(), null, OptionalInt.empty(), "foo", null, null, null)),
|
||||
Arguments.of("@c[family=normal]", new ComputerSelector("@c[family=normal]", OptionalInt.empty(), null, OptionalInt.empty(), null, ComputerFamily.NORMAL, null, null)),
|
||||
// Complex selectors
|
||||
Arguments.of("@c[ id = 123 , ]", new ComputerSelector("@c[ id = 123 , ]", OptionalInt.empty(), null, OptionalInt.of(123), null, null, null, null)),
|
||||
Arguments.of("@c[id=123,family=normal]", new ComputerSelector("@c[id=123,family=normal]", OptionalInt.empty(), null, OptionalInt.of(123), null, ComputerFamily.NORMAL, null, null)),
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void Fails_on_repeated_options() {
|
||||
var error = assertThrows(CommandSyntaxException.class, () -> ComputerSelector.parse(new StringReader("@c[id=1, id=2]")));
|
||||
assertEquals(Exceptions.ERROR_INAPPLICABLE_OPTION, error.getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void Complete_selector_components() {
|
||||
assertEquals(List.of(new Suggestion(StringRange.between(0, 1), "@c[")), suggest("@"));
|
||||
assertThat(suggest("@c["), hasItem(
|
||||
new Suggestion(StringRange.at(3), "family", ComputerSelector.options().get("family").tooltip())
|
||||
));
|
||||
assertEquals(List.of(new Suggestion(StringRange.at(9), "=")), suggest("@c[family"));
|
||||
assertEquals(List.of(new Suggestion(StringRange.at(16), ",")), suggest("@c[family=normal"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void Complete_selector_family() {
|
||||
assertThat(suggest("@c[family="), containsInAnyOrder(
|
||||
new Suggestion(StringRange.at(10), "normal"),
|
||||
new Suggestion(StringRange.at(10), "advanced"),
|
||||
new Suggestion(StringRange.at(10), "command")
|
||||
));
|
||||
assertThat(suggest("@c[family=n"), contains(
|
||||
new Suggestion(StringRange.between(10, 11), "normal")
|
||||
));
|
||||
}
|
||||
|
||||
private List<Suggestion> suggest(String input) {
|
||||
var context = new CommandContext<>(new Object(), "", Map.of(), null, null, List.of(), StringRange.at(0), null, null, false);
|
||||
return ComputerSelector.suggest(context, new SuggestionsBuilder(input, 0)).getNow(null).getList();
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ class DfpwmStateTest {
|
||||
|
||||
var state = new DfpwmState();
|
||||
state.pushBuffer(new ObjectLuaTable(inputTbl), input.length, Optional.empty());
|
||||
var result = state.pullPending(0);
|
||||
var result = state.pullPending(0).audio();
|
||||
var contents = new byte[result.remaining()];
|
||||
result.get(contents);
|
||||
|
||||
|
@ -0,0 +1,42 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.peripheral.speaker;
|
||||
|
||||
import dan200.computercraft.test.core.ArbitraryByteBuffer;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import net.jqwik.api.*;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class EncodedAudioTest {
|
||||
/**
|
||||
* Sends the audio on a roundtrip, ensuring that its contents are reassembled on the other end.
|
||||
*
|
||||
* @param audio The message to send.
|
||||
*/
|
||||
@Property
|
||||
public void testRoundTrip(@ForAll("audio") EncodedAudio audio) {
|
||||
var buffer = new FriendlyByteBuf(Unpooled.directBuffer());
|
||||
audio.write(buffer);
|
||||
|
||||
var converted = EncodedAudio.read(buffer);
|
||||
assertEquals(buffer.readableBytes(), 0, "Whole packet was read");
|
||||
|
||||
assertThat("Messages are equal", converted, equalTo(converted));
|
||||
}
|
||||
|
||||
@Provide
|
||||
Arbitrary<EncodedAudio> audio() {
|
||||
return Combinators.combine(
|
||||
Arbitraries.integers(),
|
||||
Arbitraries.integers(),
|
||||
Arbitraries.of(true, false),
|
||||
ArbitraryByteBuffer.bytes().ofMaxSize(1000)
|
||||
).as(EncodedAudio::new);
|
||||
}
|
||||
}
|
@ -11,11 +11,15 @@ import dan200.computercraft.core.apis.TermAPI
|
||||
import dan200.computercraft.core.computer.ComputerSide
|
||||
import dan200.computercraft.gametest.api.*
|
||||
import dan200.computercraft.shared.ModRegistry
|
||||
import dan200.computercraft.test.core.assertArrayEquals
|
||||
import dan200.computercraft.test.core.computer.getApi
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.gametest.framework.GameTest
|
||||
import net.minecraft.gametest.framework.GameTestHelper
|
||||
import net.minecraft.world.InteractionHand
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.item.Items
|
||||
import net.minecraft.world.level.block.Blocks
|
||||
import net.minecraft.world.level.block.LeverBlock
|
||||
import net.minecraft.world.level.block.RedstoneLampBlock
|
||||
@ -101,6 +105,17 @@ class Computer_Test {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check chest peripherals are reattached with a new size.
|
||||
*/
|
||||
@GameTest
|
||||
fun Chest_resizes_on_change(context: GameTestHelper) = context.sequence {
|
||||
thenOnComputer { callPeripheral("right", "size").assertArrayEquals(27) }
|
||||
thenExecute { context.placeItemAt(ItemStack(Items.CHEST), BlockPos(2, 2, 2), Direction.WEST) }
|
||||
thenIdle(1)
|
||||
thenOnComputer { callPeripheral("right", "size").assertArrayEquals(54) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the client can open the computer UI and interact with it.
|
||||
*/
|
||||
|
@ -7,18 +7,21 @@ package dan200.computercraft.gametest
|
||||
import dan200.computercraft.api.lua.ObjectArguments
|
||||
import dan200.computercraft.core.apis.PeripheralAPI
|
||||
import dan200.computercraft.core.computer.ComputerSide
|
||||
import dan200.computercraft.gametest.api.getBlockEntity
|
||||
import dan200.computercraft.gametest.api.sequence
|
||||
import dan200.computercraft.gametest.api.thenOnComputer
|
||||
import dan200.computercraft.gametest.api.thenStartComputer
|
||||
import dan200.computercraft.gametest.api.*
|
||||
import dan200.computercraft.impl.network.wired.WiredNodeImpl
|
||||
import dan200.computercraft.shared.ModRegistry
|
||||
import dan200.computercraft.shared.peripheral.modem.wired.CableBlock
|
||||
import dan200.computercraft.shared.peripheral.modem.wired.CableModemVariant
|
||||
import dan200.computercraft.test.core.assertArrayEquals
|
||||
import dan200.computercraft.test.core.computer.LuaTaskContext
|
||||
import dan200.computercraft.test.core.computer.getApi
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.gametest.framework.GameTest
|
||||
import net.minecraft.gametest.framework.GameTestHelper
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.item.Items
|
||||
import net.minecraft.world.level.block.Blocks
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@ -83,7 +86,86 @@ class Modem_Test {
|
||||
thenExecute {
|
||||
val modem1 = helper.getBlockEntity(BlockPos(1, 2, 1), ModRegistry.BlockEntities.WIRED_MODEM_FULL.get())
|
||||
val modem2 = helper.getBlockEntity(BlockPos(3, 2, 1), ModRegistry.BlockEntities.WIRED_MODEM_FULL.get())
|
||||
assertEquals(modem1.element.node.network, modem2.element.node.network, "On the same network")
|
||||
assertEquals((modem1.element.node as WiredNodeImpl).network, (modem2.element.node as WiredNodeImpl).network, "On the same network")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modems do not include the current peripheral when attached.
|
||||
*/
|
||||
@GameTest
|
||||
fun Cable_modem_does_not_report_self(helper: GameTestHelper) = helper.sequence {
|
||||
// Modem does not report the computer as a peripheral.
|
||||
thenOnComputer { assertEquals(listOf("back", "right"), getPeripheralNames()) }
|
||||
|
||||
// However, if we connect the network, the other modem does.
|
||||
thenExecute {
|
||||
helper.setBlock(
|
||||
BlockPos(1, 2, 3),
|
||||
ModRegistry.Blocks.CABLE.get().defaultBlockState().setValue(CableBlock.CABLE, true),
|
||||
)
|
||||
}
|
||||
thenIdle(2)
|
||||
thenOnComputer { assertEquals(listOf("back", "computer_0", "right"), getPeripheralNames()) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Modems do not include the current peripheral when attached.
|
||||
*/
|
||||
@GameTest
|
||||
fun Full_block_modem_does_not_report_self(helper: GameTestHelper) = helper.sequence {
|
||||
// Modem does not report the computer as a peripheral.
|
||||
thenOnComputer { assertEquals(listOf("back", "right"), getPeripheralNames()) }
|
||||
|
||||
// However, if we connect the network, the other modem does.
|
||||
thenExecute {
|
||||
helper.setBlock(
|
||||
BlockPos(1, 2, 3),
|
||||
ModRegistry.Blocks.CABLE.get().defaultBlockState().setValue(CableBlock.CABLE, true),
|
||||
)
|
||||
}
|
||||
thenIdle(2)
|
||||
thenOnComputer { assertEquals(listOf("back", "computer_1", "right"), getPeripheralNames()) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Test wired modems (without a cable) drop an item when the adjacent block is removed.
|
||||
*/
|
||||
@GameTest
|
||||
fun Modem_drops_when_neighbour_removed(helper: GameTestHelper) = helper.sequence {
|
||||
thenExecute {
|
||||
helper.setBlock(BlockPos(2, 3, 2), Blocks.AIR)
|
||||
helper.assertItemEntityPresent(ModRegistry.Items.WIRED_MODEM.get(), BlockPos(2, 2, 2), 0.0)
|
||||
helper.assertBlockPresent(Blocks.AIR, BlockPos(2, 2, 2))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test wired modems (with a cable) drop an item, but keep their cable when the adjacent block is removed.
|
||||
*/
|
||||
@GameTest
|
||||
fun Modem_keeps_cable_when_neighbour_removed(helper: GameTestHelper) = helper.sequence {
|
||||
thenExecute {
|
||||
helper.setBlock(BlockPos(2, 3, 2), Blocks.AIR)
|
||||
helper.assertItemEntityPresent(ModRegistry.Items.WIRED_MODEM.get(), BlockPos(2, 2, 2), 0.0)
|
||||
helper.assertBlockIs(BlockPos(2, 2, 2)) {
|
||||
it.block == ModRegistry.Blocks.CABLE.get() && it.getValue(CableBlock.MODEM) == CableModemVariant.None && it.getValue(CableBlock.CABLE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check chest peripherals are reattached with a new size.
|
||||
*/
|
||||
@GameTest
|
||||
fun Chest_resizes_on_change(context: GameTestHelper) = context.sequence {
|
||||
thenOnComputer {
|
||||
callRemotePeripheral("minecraft:chest_0", "size").assertArrayEquals(27)
|
||||
}
|
||||
thenExecute { context.placeItemAt(ItemStack(Items.CHEST), BlockPos(2, 2, 2), Direction.WEST) }
|
||||
thenIdle(1)
|
||||
thenOnComputer {
|
||||
callRemotePeripheral("minecraft:chest_0", "size").assertArrayEquals(54)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -105,7 +187,7 @@ private suspend fun LuaTaskContext.getPeripheralNames(): List<String> {
|
||||
if (!peripheral.isPresent(side)) continue
|
||||
peripherals.add(side)
|
||||
|
||||
val hasType = peripheral.hasType(side, "modem")
|
||||
val hasType = peripheral.hasType(side, "peripheral_hub")
|
||||
if (hasType == null || hasType[0] != true) continue
|
||||
|
||||
val names = peripheral.call(context, ObjectArguments(side, "getNamesRemote")).await() ?: continue
|
||||
@ -116,3 +198,22 @@ private suspend fun LuaTaskContext.getPeripheralNames(): List<String> {
|
||||
peripherals.sort()
|
||||
return peripherals
|
||||
}
|
||||
|
||||
private suspend fun LuaTaskContext.callRemotePeripheral(name: String, method: String, vararg args: Any): Array<out Any?>? {
|
||||
val peripheral = getApi<PeripheralAPI>()
|
||||
if (peripheral.isPresent(name)) return peripheral.call(context, ObjectArguments(name, method, *args)).await()
|
||||
|
||||
for (side in ComputerSide.NAMES) {
|
||||
if (!peripheral.isPresent(side)) continue
|
||||
|
||||
val hasType = peripheral.hasType(side, "peripheral_hub")
|
||||
if (hasType == null || hasType[0] != true) continue
|
||||
|
||||
val isPresent = peripheral.call(context, ObjectArguments(side, "isPresentRemote", name)).await() ?: continue
|
||||
if (isPresent[0] as Boolean) {
|
||||
return peripheral.call(context, ObjectArguments(side, "callRemote", name, method, *args)).await()
|
||||
}
|
||||
}
|
||||
|
||||
throw IllegalArgumentException("No such peripheral $name")
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ class Pocket_Computer_Test {
|
||||
// And ensure its synced to the client.
|
||||
thenIdle(4)
|
||||
thenOnClient {
|
||||
val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)
|
||||
val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)!!
|
||||
assertEquals(ComputerState.ON, pocketComputer.state)
|
||||
|
||||
val term = pocketComputer.terminal
|
||||
@ -54,7 +54,7 @@ class Pocket_Computer_Test {
|
||||
// And ensure the new computer state and terminal are sent.
|
||||
thenIdle(4)
|
||||
thenOnClient {
|
||||
val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)
|
||||
val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)!!
|
||||
assertEquals(ComputerState.BLINKING, pocketComputer.state)
|
||||
|
||||
val term = pocketComputer.terminal
|
||||
|
@ -329,8 +329,6 @@ class Turtle_Test {
|
||||
|
||||
/**
|
||||
* Checks turtles can be cleaned in cauldrons.
|
||||
*
|
||||
* Currently not required as turtles can no longer right-click cauldrons.
|
||||
*/
|
||||
@GameTest
|
||||
fun Cleaned_with_cauldrons(helper: GameTestHelper) = helper.sequence {
|
||||
@ -643,7 +641,20 @@ class Turtle_Test {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Turtle sucking from items
|
||||
/**
|
||||
* `turtle.suck` only pulls for the current side.
|
||||
*/
|
||||
@GameTest
|
||||
fun Sided_suck(helper: GameTestHelper) = helper.sequence {
|
||||
thenOnComputer {
|
||||
turtle.suckUp(Optional.empty()).await().assertArrayEquals(true)
|
||||
turtle.getItemDetail(context, Optional.empty(), Optional.empty()).await().assertArrayEquals(
|
||||
mapOf("name" to "minecraft:iron_ingot", "count" to 8),
|
||||
)
|
||||
|
||||
turtle.suckUp(Optional.empty()).await().assertArrayEquals(false, "No items to take")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render turtles as an item.
|
||||
|
@ -20,15 +20,19 @@ import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.gametest.framework.*
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.world.Container
|
||||
import net.minecraft.world.InteractionHand
|
||||
import net.minecraft.world.entity.Entity
|
||||
import net.minecraft.world.entity.EntityType
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.item.context.UseOnContext
|
||||
import net.minecraft.world.level.block.Blocks
|
||||
import net.minecraft.world.level.block.entity.BarrelBlockEntity
|
||||
import net.minecraft.world.level.block.entity.BlockEntity
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.level.block.state.properties.Property
|
||||
import net.minecraft.world.phys.BlockHitResult
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import org.hamcrest.Matchers
|
||||
import org.hamcrest.StringDescription
|
||||
|
||||
@ -306,3 +310,16 @@ fun GameTestHelper.setContainerItem(pos: BlockPos, slot: Int, item: ItemStack) {
|
||||
container.setItem(slot, item)
|
||||
container.setChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* An alternative version ot [GameTestHelper.placeAt], which sets the player's held item first.
|
||||
*
|
||||
* This is required for compatibility with Forge, which uses the in-hand stack, rather than the stack requested.
|
||||
*/
|
||||
fun GameTestHelper.placeItemAt(stack: ItemStack, pos: BlockPos, direction: Direction) {
|
||||
val player = makeMockPlayer()
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, stack)
|
||||
val absolutePos = absolutePos(pos.relative(direction))
|
||||
val hit = BlockHitResult(Vec3.atCenterOf(absolutePos), direction, absolutePos, false)
|
||||
stack.useOn(UseOnContext(player, InteractionHand.MAIN_HAND, hit))
|
||||
}
|
||||
|
138
projects/common/src/testMod/resources/data/cctest/structures/computer_test.chest_resizes_on_change.snbt
generated
Normal file
138
projects/common/src/testMod/resources/data/cctest/structures/computer_test.chest_resizes_on_change.snbt
generated
Normal file
@ -0,0 +1,138 @@
|
||||
{
|
||||
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: "minecraft:chest{facing:north,type:single,waterlogged:false}", nbt: {Items: [], id: "minecraft:chest"}},
|
||||
{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:computer_normal{facing:north,state:on}", nbt: {ComputerId: 1, Label: "computer_test.chest_resizes_on_change", On: 1b, id: "computercraft:computer_normal"}},
|
||||
{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",
|
||||
"minecraft:chest{facing:north,type:single,waterlogged:false}",
|
||||
"computercraft:computer_normal{facing:north,state:on}"
|
||||
]
|
||||
}
|
@ -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:cable{cable:true,down:false,east:true,modem:east_off_peripheral,north:false,south:false,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 1b, PeripheralId: 0, PeripheralType: "computer", id: "computercraft:cable"}},
|
||||
{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:computer_normal{facing:north,state:on}", nbt: {ComputerId: 1, Label: "modem_test.cable_modem_does_not_report_self", On: 1b, id: "computercraft:computer_normal"}},
|
||||
{pos: [2, 1, 3], state: "computercraft:cable{cable:true,down:false,east:false,modem:north_off,north:true,south:false,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
|
||||
{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:cable{cable:true,down:false,east:true,modem:east_off_peripheral,north:false,south:false,up:false,waterlogged:false,west:false}",
|
||||
"computercraft:computer_normal{facing:north,state:on}",
|
||||
"computercraft:cable{cable:true,down:false,east:false,modem:north_off,north:true,south:false,up:false,waterlogged:false,west:false}"
|
||||
]
|
||||
}
|
139
projects/common/src/testMod/resources/data/cctest/structures/modem_test.chest_resizes_on_change.snbt
generated
Normal file
139
projects/common/src/testMod/resources/data/cctest/structures/modem_test.chest_resizes_on_change.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: "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: "minecraft:chest{facing:north,type:single,waterlogged:false}", nbt: {Items: [], id: "minecraft:chest"}},
|
||||
{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: "computercraft:computer_normal{facing:north,state:on}", nbt: {ComputerId: 1, Label: "modem_test.chest_resizes_on_change", On: 1b, id: "computercraft:computer_normal"}},
|
||||
{pos: [3, 1, 2], state: "computercraft:wired_modem_full{modem:false,peripheral:true}", nbt: {PeripheralId2: 2, PeripheralId4: 0, PeripheralType2: "computer", PeripheralType4: "minecraft:chest", id: "computercraft:wired_modem_full"}},
|
||||
{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",
|
||||
"minecraft:chest{facing:north,type:single,waterlogged:false}",
|
||||
"computercraft:computer_normal{facing:north,state:on}",
|
||||
"computercraft:wired_modem_full{modem:false,peripheral:true}"
|
||||
]
|
||||
}
|
@ -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:wired_modem_full{modem:false,peripheral:true}", nbt: {PeripheralAccess: 1b, PeripheralId5: 1, PeripheralType5: "computer", id: "computercraft:wired_modem_full"}},
|
||||
{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:computer_normal{facing:north,state:on}", nbt: {ComputerId: 1, Label: "modem_test.full_block_modem_does_not_report_self", On: 1b, id: "computercraft:computer_normal"}},
|
||||
{pos: [2, 1, 3], state: "computercraft:wired_modem_full{modem:false,peripheral:false}", nbt: {PeripheralAccess: 0b, id: "computercraft:wired_modem_full"}},
|
||||
{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:wired_modem_full{modem:false,peripheral:true}",
|
||||
"computercraft:computer_normal{facing:north,state:on}",
|
||||
"computercraft:wired_modem_full{modem:false,peripheral:false}"
|
||||
]
|
||||
}
|
@ -28,7 +28,7 @@
|
||||
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
|
||||
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
|
||||
{pos: [0, 1, 0], state: "computercraft:printer{bottom:false,facing:north,top:false}", nbt: {Items: [], PageTitle: "", Printing: 0b, id: "computercraft:printer", term_bgColour: 15, term_cursorBlink: 0b, term_cursorX: 0, term_cursorY: 0, term_palette: [I; 1118481, 13388876, 5744206, 8349260, 3368652, 11691749, 5020082, 10066329, 5000268, 15905484, 8375321, 14605932, 10072818, 15040472, 15905331, 15790320], term_textBgColour_0: "fffffffffffffffffffffffff", term_textBgColour_1: "fffffffffffffffffffffffff", term_textBgColour_10: "fffffffffffffffffffffffff", term_textBgColour_11: "fffffffffffffffffffffffff", term_textBgColour_12: "fffffffffffffffffffffffff", term_textBgColour_13: "fffffffffffffffffffffffff", term_textBgColour_14: "fffffffffffffffffffffffff", term_textBgColour_15: "fffffffffffffffffffffffff", term_textBgColour_16: "fffffffffffffffffffffffff", term_textBgColour_17: "fffffffffffffffffffffffff", term_textBgColour_18: "fffffffffffffffffffffffff", term_textBgColour_19: "fffffffffffffffffffffffff", term_textBgColour_2: "fffffffffffffffffffffffff", term_textBgColour_20: "fffffffffffffffffffffffff", term_textBgColour_3: "fffffffffffffffffffffffff", term_textBgColour_4: "fffffffffffffffffffffffff", term_textBgColour_5: "fffffffffffffffffffffffff", term_textBgColour_6: "fffffffffffffffffffffffff", term_textBgColour_7: "fffffffffffffffffffffffff", term_textBgColour_8: "fffffffffffffffffffffffff", term_textBgColour_9: "fffffffffffffffffffffffff", term_textColour: 0, term_textColour_0: "0000000000000000000000000", term_textColour_1: "0000000000000000000000000", term_textColour_10: "0000000000000000000000000", term_textColour_11: "0000000000000000000000000", term_textColour_12: "0000000000000000000000000", term_textColour_13: "0000000000000000000000000", term_textColour_14: "0000000000000000000000000", term_textColour_15: "0000000000000000000000000", term_textColour_16: "0000000000000000000000000", term_textColour_17: "0000000000000000000000000", term_textColour_18: "0000000000000000000000000", term_textColour_19: "0000000000000000000000000", term_textColour_2: "0000000000000000000000000", term_textColour_20: "0000000000000000000000000", term_textColour_3: "0000000000000000000000000", term_textColour_4: "0000000000000000000000000", term_textColour_5: "0000000000000000000000000", term_textColour_6: "0000000000000000000000000", term_textColour_7: "0000000000000000000000000", term_textColour_8: "0000000000000000000000000", term_textColour_9: "0000000000000000000000000", term_text_0: " ", term_text_1: " ", term_text_10: " ", term_text_11: " ", term_text_12: " ", term_text_13: " ", term_text_14: " ", term_text_15: " ", term_text_16: " ", term_text_17: " ", term_text_18: " ", term_text_19: " ", term_text_2: " ", term_text_20: " ", term_text_3: " ", term_text_4: " ", term_text_5: " ", term_text_6: " ", term_text_7: " ", term_text_8: " ", term_text_9: " "}},
|
||||
{pos: [0, 1, 1], state: "computercraft:cable{cable:true,down:false,east:true,modem:north_on,north:true,south:false,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 1b, PeripheralId: 1, PeripheralType: "printer", id: "computercraft:cable"}},
|
||||
{pos: [0, 1, 1], state: "computercraft:cable{cable:true,down:false,east:true,modem:north_off_peripheral,north:true,south:false,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 1b, PeripheralId: 1, PeripheralType: "printer", id: "computercraft:cable"}},
|
||||
{pos: [0, 1, 2], state: "minecraft:air"},
|
||||
{pos: [0, 1, 3], state: "minecraft:air"},
|
||||
{pos: [0, 1, 4], state: "computercraft:monitor_advanced{facing:north,orientation:north,state:none}", nbt: {Height: 1, Width: 1, XIndex: 0, YIndex: 0, id: "computercraft:monitor_advanced"}},
|
||||
@ -36,7 +36,7 @@
|
||||
{pos: [1, 1, 1], state: "computercraft:cable{cable:true,down:false,east:false,modem:none,north:false,south:true,up:false,waterlogged:false,west:true}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
|
||||
{pos: [1, 1, 2], state: "computercraft:cable{cable:true,down:false,east:false,modem:none,north:true,south:true,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
|
||||
{pos: [1, 1, 3], state: "computercraft:cable{cable:true,down:false,east:false,modem:none,north:true,south:true,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
|
||||
{pos: [1, 1, 4], state: "computercraft:cable{cable:true,down:false,east:false,modem:west_on,north:true,south:false,up:false,waterlogged:false,west:true}", nbt: {PeripheralAccess: 1b, PeripheralId: 1, PeripheralType: "monitor", id: "computercraft:cable"}},
|
||||
{pos: [1, 1, 4], state: "computercraft:cable{cable:true,down:false,east:false,modem:west_off_peripheral,north:true,south:false,up:false,waterlogged:false,west:true}", nbt: {PeripheralAccess: 1b, PeripheralId: 1, PeripheralType: "monitor", id: "computercraft:cable"}},
|
||||
{pos: [2, 1, 0], state: "minecraft:air"},
|
||||
{pos: [2, 1, 1], state: "minecraft:air"},
|
||||
{pos: [2, 1, 2], state: "minecraft:air"},
|
||||
@ -133,11 +133,11 @@
|
||||
"minecraft:polished_andesite",
|
||||
"minecraft:air",
|
||||
"computercraft:printer{bottom:false,facing:north,top:false}",
|
||||
"computercraft:cable{cable:true,down:false,east:true,modem:north_on,north:true,south:false,up:false,waterlogged:false,west:false}",
|
||||
"computercraft:cable{cable:true,down:false,east:true,modem:north_off_peripheral,north:true,south:false,up:false,waterlogged:false,west:false}",
|
||||
"computercraft:monitor_advanced{facing:north,orientation:north,state:none}",
|
||||
"computercraft:cable{cable:true,down:false,east:false,modem:none,north:false,south:true,up:false,waterlogged:false,west:true}",
|
||||
"computercraft:cable{cable:true,down:false,east:false,modem:none,north:true,south:true,up:false,waterlogged:false,west:false}",
|
||||
"computercraft:cable{cable:true,down:false,east:false,modem:west_on,north:true,south:false,up:false,waterlogged:false,west:true}",
|
||||
"computercraft:cable{cable:true,down:false,east:false,modem:west_off_peripheral,north:true,south:false,up:false,waterlogged:false,west:true}",
|
||||
"computercraft:cable{cable:true,down:false,east:true,modem:none,north:false,south:false,up:false,waterlogged:false,west:false}",
|
||||
"computercraft:computer_advanced{facing:north,state:blinking}",
|
||||
"computercraft:cable{cable:true,down:false,east:false,modem:north_off,north:true,south:false,up:false,waterlogged:false,west:true}"
|
||||
|
@ -28,7 +28,7 @@
|
||||
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
|
||||
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
|
||||
{pos: [0, 1, 0], state: "computercraft:printer{bottom:false,facing:north,top:false}", nbt: {Items: [], PageTitle: "", Printing: 0b, id: "computercraft:printer", term_bgColour: 15, term_cursorBlink: 0b, term_cursorX: 0, term_cursorY: 0, term_palette: [I; 1118481, 13388876, 5744206, 8349260, 3368652, 11691749, 5020082, 10066329, 5000268, 15905484, 8375321, 14605932, 10072818, 15040472, 15905331, 15790320], term_textBgColour_0: "fffffffffffffffffffffffff", term_textBgColour_1: "fffffffffffffffffffffffff", term_textBgColour_10: "fffffffffffffffffffffffff", term_textBgColour_11: "fffffffffffffffffffffffff", term_textBgColour_12: "fffffffffffffffffffffffff", term_textBgColour_13: "fffffffffffffffffffffffff", term_textBgColour_14: "fffffffffffffffffffffffff", term_textBgColour_15: "fffffffffffffffffffffffff", term_textBgColour_16: "fffffffffffffffffffffffff", term_textBgColour_17: "fffffffffffffffffffffffff", term_textBgColour_18: "fffffffffffffffffffffffff", term_textBgColour_19: "fffffffffffffffffffffffff", term_textBgColour_2: "fffffffffffffffffffffffff", term_textBgColour_20: "fffffffffffffffffffffffff", term_textBgColour_3: "fffffffffffffffffffffffff", term_textBgColour_4: "fffffffffffffffffffffffff", term_textBgColour_5: "fffffffffffffffffffffffff", term_textBgColour_6: "fffffffffffffffffffffffff", term_textBgColour_7: "fffffffffffffffffffffffff", term_textBgColour_8: "fffffffffffffffffffffffff", term_textBgColour_9: "fffffffffffffffffffffffff", term_textColour: 0, term_textColour_0: "0000000000000000000000000", term_textColour_1: "0000000000000000000000000", term_textColour_10: "0000000000000000000000000", term_textColour_11: "0000000000000000000000000", term_textColour_12: "0000000000000000000000000", term_textColour_13: "0000000000000000000000000", term_textColour_14: "0000000000000000000000000", term_textColour_15: "0000000000000000000000000", term_textColour_16: "0000000000000000000000000", term_textColour_17: "0000000000000000000000000", term_textColour_18: "0000000000000000000000000", term_textColour_19: "0000000000000000000000000", term_textColour_2: "0000000000000000000000000", term_textColour_20: "0000000000000000000000000", term_textColour_3: "0000000000000000000000000", term_textColour_4: "0000000000000000000000000", term_textColour_5: "0000000000000000000000000", term_textColour_6: "0000000000000000000000000", term_textColour_7: "0000000000000000000000000", term_textColour_8: "0000000000000000000000000", term_textColour_9: "0000000000000000000000000", term_text_0: " ", term_text_1: " ", term_text_10: " ", term_text_11: " ", term_text_12: " ", term_text_13: " ", term_text_14: " ", term_text_15: " ", term_text_16: " ", term_text_17: " ", term_text_18: " ", term_text_19: " ", term_text_2: " ", term_text_20: " ", term_text_3: " ", term_text_4: " ", term_text_5: " ", term_text_6: " ", term_text_7: " ", term_text_8: " ", term_text_9: " "}},
|
||||
{pos: [0, 1, 1], state: "computercraft:cable{cable:true,down:false,east:false,modem:north_on,north:true,south:true,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 1b, PeripheralId: 0, PeripheralType: "printer", id: "computercraft:cable"}},
|
||||
{pos: [0, 1, 1], state: "computercraft:cable{cable:true,down:false,east:false,modem:north_off_peripheral,north:true,south:true,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 1b, PeripheralId: 0, PeripheralType: "printer", id: "computercraft:cable"}},
|
||||
{pos: [0, 1, 2], state: "computercraft:cable{cable:true,down:false,east:true,modem:none,north:true,south:false,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
|
||||
{pos: [0, 1, 3], state: "minecraft:air"},
|
||||
{pos: [0, 1, 4], state: "computercraft:monitor_advanced{facing:north,orientation:north,state:none}", nbt: {Height: 1, Width: 1, XIndex: 0, YIndex: 0, id: "computercraft:monitor_advanced"}},
|
||||
@ -36,7 +36,7 @@
|
||||
{pos: [1, 1, 1], state: "minecraft:air"},
|
||||
{pos: [1, 1, 2], state: "computercraft:cable{cable:true,down:false,east:true,modem:none,north:false,south:true,up:false,waterlogged:false,west:true}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
|
||||
{pos: [1, 1, 3], state: "computercraft:cable{cable:true,down:false,east:false,modem:none,north:true,south:true,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
|
||||
{pos: [1, 1, 4], state: "computercraft:cable{cable:true,down:false,east:false,modem:west_on,north:true,south:false,up:false,waterlogged:false,west:true}", nbt: {PeripheralAccess: 1b, PeripheralId: 0, PeripheralType: "monitor", id: "computercraft:cable"}},
|
||||
{pos: [1, 1, 4], state: "computercraft:cable{cable:true,down:false,east:false,modem:west_off_peripheral,north:true,south:false,up:false,waterlogged:false,west:true}", nbt: {PeripheralAccess: 1b, PeripheralId: 0, PeripheralType: "monitor", id: "computercraft:cable"}},
|
||||
{pos: [2, 1, 0], state: "minecraft:air"},
|
||||
{pos: [2, 1, 1], state: "minecraft:air"},
|
||||
{pos: [2, 1, 2], state: "computercraft:cable{cable:true,down:false,east:true,modem:none,north:false,south:false,up:false,waterlogged:false,west:true}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
|
||||
@ -133,12 +133,12 @@
|
||||
"minecraft:polished_andesite",
|
||||
"minecraft:air",
|
||||
"computercraft:printer{bottom:false,facing:north,top:false}",
|
||||
"computercraft:cable{cable:true,down:false,east:false,modem:north_on,north:true,south:true,up:false,waterlogged:false,west:false}",
|
||||
"computercraft:cable{cable:true,down:false,east:false,modem:north_off_peripheral,north:true,south:true,up:false,waterlogged:false,west:false}",
|
||||
"computercraft:cable{cable:true,down:false,east:true,modem:none,north:true,south:false,up:false,waterlogged:false,west:false}",
|
||||
"computercraft:monitor_advanced{facing:north,orientation:north,state:none}",
|
||||
"computercraft:cable{cable:true,down:false,east:true,modem:none,north:false,south:true,up:false,waterlogged:false,west:true}",
|
||||
"computercraft:cable{cable:true,down:false,east:false,modem:none,north:true,south:true,up:false,waterlogged:false,west:false}",
|
||||
"computercraft:cable{cable:true,down:false,east:false,modem:west_on,north:true,south:false,up:false,waterlogged:false,west:true}",
|
||||
"computercraft:cable{cable:true,down:false,east:false,modem:west_off_peripheral,north:true,south:false,up:false,waterlogged:false,west:true}",
|
||||
"computercraft:cable{cable:true,down:false,east:true,modem:none,north:false,south:false,up:false,waterlogged:false,west:true}",
|
||||
"computercraft:cable{cable:true,down:false,east:true,modem:east_off,north:false,south:false,up:false,waterlogged:false,west:true}",
|
||||
"computercraft:computer_advanced{facing:north,state:blinking}"
|
||||
|
@ -0,0 +1,138 @@
|
||||
{
|
||||
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:light_gray_stained_glass"},
|
||||
{pos: [1, 1, 2], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [1, 1, 3], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [1, 1, 4], state: "minecraft:air"},
|
||||
{pos: [2, 1, 0], state: "minecraft:air"},
|
||||
{pos: [2, 1, 1], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [2, 1, 2], state: "computercraft:cable{cable:false,down:false,east:false,modem:up_off,north:false,south:false,up:false,waterlogged:false,west:false}", nbt: {id: "computercraft:cable"}},
|
||||
{pos: [2, 1, 3], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [2, 1, 4], state: "minecraft:air"},
|
||||
{pos: [3, 1, 0], state: "minecraft:air"},
|
||||
{pos: [3, 1, 1], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [3, 1, 2], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [3, 1, 3], state: "minecraft:light_gray_stained_glass"},
|
||||
{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:light_gray_stained_glass"},
|
||||
{pos: [1, 2, 2], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [1, 2, 3], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [1, 2, 4], state: "minecraft:air"},
|
||||
{pos: [2, 2, 0], state: "minecraft:air"},
|
||||
{pos: [2, 2, 1], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [2, 2, 2], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [2, 2, 3], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [2, 2, 4], state: "minecraft:air"},
|
||||
{pos: [3, 2, 0], state: "minecraft:air"},
|
||||
{pos: [3, 2, 1], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [3, 2, 2], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [3, 2, 3], state: "minecraft:light_gray_stained_glass"},
|
||||
{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:light_gray_stained_glass",
|
||||
"minecraft:air",
|
||||
"computercraft:cable{cable:false,down:false,east:false,modem:up_off,north:false,south:false,up:false,waterlogged:false,west:false}"
|
||||
]
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
{
|
||||
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:light_gray_stained_glass"},
|
||||
{pos: [1, 1, 2], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [1, 1, 3], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [1, 1, 4], state: "minecraft:air"},
|
||||
{pos: [2, 1, 0], state: "minecraft:air"},
|
||||
{pos: [2, 1, 1], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [2, 1, 2], state: "computercraft:cable{cable:true,down:false,east:false,modem:up_off,north:false,south:false,up:true,waterlogged:false,west:false}", nbt: {id: "computercraft:cable"}},
|
||||
{pos: [2, 1, 3], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [2, 1, 4], state: "minecraft:air"},
|
||||
{pos: [3, 1, 0], state: "minecraft:air"},
|
||||
{pos: [3, 1, 1], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [3, 1, 2], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [3, 1, 3], state: "minecraft:light_gray_stained_glass"},
|
||||
{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:light_gray_stained_glass"},
|
||||
{pos: [1, 2, 2], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [1, 2, 3], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [1, 2, 4], state: "minecraft:air"},
|
||||
{pos: [2, 2, 0], state: "minecraft:air"},
|
||||
{pos: [2, 2, 1], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [2, 2, 2], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [2, 2, 3], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [2, 2, 4], state: "minecraft:air"},
|
||||
{pos: [3, 2, 0], state: "minecraft:air"},
|
||||
{pos: [3, 2, 1], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [3, 2, 2], state: "minecraft:light_gray_stained_glass"},
|
||||
{pos: [3, 2, 3], state: "minecraft:light_gray_stained_glass"},
|
||||
{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:light_gray_stained_glass",
|
||||
"minecraft:air",
|
||||
"computercraft:cable{cable:true,down:false,east:false,modem:up_off,north:false,south:false,up:true,waterlogged:false,west:false}"
|
||||
]
|
||||
}
|
138
projects/common/src/testMod/resources/data/cctest/structures/turtle_test.sided_suck.snbt
generated
Normal file
138
projects/common/src/testMod/resources/data/cctest/structures/turtle_test.sided_suck.snbt
generated
Normal file
@ -0,0 +1,138 @@
|
||||
{
|
||||
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: [], Label: "turtle_test.sided_suck", On: 1b, 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:furnace{facing:north,lit:false}", nbt: {BurnTime: 0s, CookTime: 0s, CookTimeTotal: 200s, Items: [{Count: 64b, Slot: 1b, id: "minecraft:coal"}, {Count: 8b, Slot: 2b, id: "minecraft:iron_ingot"}], RecipesUsed: {"minecraft:iron_ingot_from_smelting_raw_iron": 8}, id: "minecraft:furnace"}},
|
||||
{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}",
|
||||
"minecraft:furnace{facing:north,lit:false}"
|
||||
]
|
||||
}
|
@ -116,6 +116,7 @@ public final class MethodResult {
|
||||
* @return The method result which represents this yield.
|
||||
* @see #pullEvent(String, ILuaCallback)
|
||||
*/
|
||||
@SuppressWarnings("NamedLikeContextualKeyword")
|
||||
public static MethodResult yield(@Nullable Object[] arguments, ILuaCallback callback) {
|
||||
Objects.requireNonNull(callback, "callback cannot be null");
|
||||
return new MethodResult(arguments, callback);
|
||||
|
@ -11,6 +11,7 @@ import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.api.peripheral.NotAttachedException;
|
||||
import dan200.computercraft.api.peripheral.WorkMonitor;
|
||||
import dan200.computercraft.core.computer.ComputerSide;
|
||||
import dan200.computercraft.core.computer.GuardedLuaContext;
|
||||
import dan200.computercraft.core.methods.MethodSupplier;
|
||||
import dan200.computercraft.core.methods.PeripheralMethod;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
@ -26,7 +27,7 @@ import java.util.*;
|
||||
* @hidden
|
||||
*/
|
||||
public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChangeListener {
|
||||
private class PeripheralWrapper extends ComputerAccess {
|
||||
private class PeripheralWrapper extends ComputerAccess implements GuardedLuaContext.Guard {
|
||||
private final String side;
|
||||
private final IPeripheral peripheral;
|
||||
|
||||
@ -35,6 +36,8 @@ public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange
|
||||
private final Map<String, PeripheralMethod> methodMap;
|
||||
private boolean attached = false;
|
||||
|
||||
private @Nullable GuardedLuaContext contextWrapper;
|
||||
|
||||
PeripheralWrapper(IPeripheral peripheral, String side) {
|
||||
super(environment);
|
||||
this.side = side;
|
||||
@ -91,9 +94,21 @@ public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange
|
||||
|
||||
if (method == null) throw new LuaException("No such method " + methodName);
|
||||
|
||||
try (var ignored = environment.time(Metrics.PERIPHERAL_OPS)) {
|
||||
return method.apply(peripheral, context, this, arguments);
|
||||
// Wrap the ILuaContext. We try to reuse the previous context where possible to avoid allocations - this
|
||||
// should be pretty common as ILuaMachine uses a constant context.
|
||||
var contextWrapper = this.contextWrapper;
|
||||
if (contextWrapper == null || !contextWrapper.wraps(context)) {
|
||||
contextWrapper = this.contextWrapper = new GuardedLuaContext(context, this);
|
||||
}
|
||||
|
||||
try (var ignored = environment.time(Metrics.PERIPHERAL_OPS)) {
|
||||
return method.apply(peripheral, contextWrapper, this, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkValid() {
|
||||
return isAttached();
|
||||
}
|
||||
|
||||
// IComputerAccess implementation
|
||||
|
@ -13,8 +13,54 @@ import dan200.computercraft.core.util.Colour;
|
||||
|
||||
|
||||
/**
|
||||
* Interact with a computer's terminal or monitors, writing text and drawing
|
||||
* ASCII graphics.
|
||||
* Interact with a computer's terminal or monitors, writing text and drawing ASCII graphics.
|
||||
*
|
||||
* <h2>Writing to the terminal</h2>
|
||||
* The simplest operation one can perform on a terminal is displaying (or writing) some text. This can be performed with
|
||||
* the [`term.write`] method.
|
||||
*
|
||||
* <pre>{@code
|
||||
* term.write("Hello, world!")
|
||||
* }</pre>
|
||||
* <p>
|
||||
* When you write text, this advances the cursor, so the next call to [`term.write`] will write text immediately after
|
||||
* the previous one.
|
||||
*
|
||||
* <pre>{@code
|
||||
* term.write("Hello, world!")
|
||||
* term.write("Some more text")
|
||||
* }</pre>
|
||||
* <p>
|
||||
* [`term.getCursorPos`] and [`term.setCursorPos`] can be used to manually change the cursor's position.
|
||||
* <p>
|
||||
* <pre>{@code
|
||||
* term.clear()
|
||||
*
|
||||
* term.setCursorPos(1, 1) -- The first column of line 1
|
||||
* term.write("First line")
|
||||
*
|
||||
* term.setCursorPos(20, 2) -- The 20th column of line 2
|
||||
* term.write("Second line")
|
||||
* }</pre>
|
||||
* <p>
|
||||
* [`term.write`] is a relatively basic and low-level function, and does not handle more advanced features such as line
|
||||
* breaks or word wrapping. If you just want to display text to the screen, you probably want to use [`print`] or
|
||||
* [`write`] instead.
|
||||
*
|
||||
* <h2>Colours</h2>
|
||||
* So far we've been writing text in black and white. However, advanced computers are also capable of displaying text
|
||||
* in a variety of colours, with the [`term.setTextColour`] and [`term.setBackgroundColour`] functions.
|
||||
*
|
||||
* <pre>{@code
|
||||
* print("This text is white")
|
||||
* term.setTextColour(colours.green)
|
||||
* print("This text is green")
|
||||
* }</pre>
|
||||
* <p>
|
||||
* These functions accept any of the constants from the [`colors`] API. [Combinations of colours][`colors.combine`] may
|
||||
* be accepted, but will only display a single colour (typically following the behaviour of [`colors.toBlit`]).
|
||||
* <p>
|
||||
* The [`paintutils`] API provides several helpful functions for displaying graphics using [`term.setBackgroundColour`].
|
||||
*
|
||||
* @cc.module term
|
||||
*/
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
package dan200.computercraft.core.apis.http.options;
|
||||
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
|
||||
/**
|
||||
* Options for a given HTTP request or websocket, which control its resource constraints.
|
||||
@ -14,5 +15,6 @@ package dan200.computercraft.core.apis.http.options;
|
||||
* @param websocketMessage The maximum size of a websocket message (outgoing and incoming).
|
||||
* @param useProxy Whether to use the configured proxy.
|
||||
*/
|
||||
@Immutable
|
||||
public record Options(Action action, long maxUpload, long maxDownload, int websocketMessage, boolean useProxy) {
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
package dan200.computercraft.core.apis.http.options;
|
||||
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
import com.google.errorprone.annotations.concurrent.LazyInit;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Optional;
|
||||
@ -23,7 +24,7 @@ public final class PartialOptions {
|
||||
private final OptionalInt websocketMessage;
|
||||
private final Optional<Boolean> useProxy;
|
||||
|
||||
@SuppressWarnings("Immutable") // Lazily initialised, so this mutation is invisible in the public API
|
||||
@LazyInit
|
||||
private @Nullable Options options;
|
||||
|
||||
public PartialOptions(@Nullable Action action, OptionalLong maxUpload, OptionalLong maxDownload, OptionalInt websocketMessage, Optional<Boolean> useProxy) {
|
||||
|
@ -27,7 +27,7 @@ final class ResultHelpers {
|
||||
return throwUnchecked0(t);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@SuppressWarnings({ "unchecked", "TypeParameterUnusedInFormals" })
|
||||
private static <T extends Throwable> T throwUnchecked0(Throwable t) throws T {
|
||||
throw (T) t;
|
||||
}
|
||||
|
@ -15,8 +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.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
@ -54,9 +53,9 @@ public class Computer {
|
||||
private final AtomicLong lastTaskId = new AtomicLong();
|
||||
|
||||
// Additional state about the computer and its environment.
|
||||
private boolean blinking = false;
|
||||
private final Environment internalEnvironment;
|
||||
private final AtomicBoolean externalOutputChanged = new AtomicBoolean();
|
||||
|
||||
private final AtomicInteger externalOutputChanges = new AtomicInteger();
|
||||
|
||||
private boolean startRequested;
|
||||
private int ticksSinceStart = -1;
|
||||
@ -140,10 +139,7 @@ public class Computer {
|
||||
}
|
||||
|
||||
public void setLabel(@Nullable String label) {
|
||||
if (!Objects.equals(label, this.label)) {
|
||||
this.label = label;
|
||||
externalOutputChanged.set(true);
|
||||
}
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public void tick() {
|
||||
@ -164,28 +160,24 @@ public class Computer {
|
||||
internalEnvironment.tick();
|
||||
|
||||
// Propagate the environment's output to the world.
|
||||
if (internalEnvironment.updateOutput()) externalOutputChanged.set(true);
|
||||
|
||||
// Set output changed if the terminal has changed from blinking to not
|
||||
var blinking = terminal.getCursorBlink() &&
|
||||
terminal.getCursorX() >= 0 && terminal.getCursorX() < terminal.getWidth() &&
|
||||
terminal.getCursorY() >= 0 && terminal.getCursorY() < terminal.getHeight();
|
||||
if (blinking != this.blinking) {
|
||||
this.blinking = blinking;
|
||||
externalOutputChanged.set(true);
|
||||
}
|
||||
externalOutputChanges.accumulateAndGet(internalEnvironment.updateOutput(), (x, y) -> x | y);
|
||||
}
|
||||
|
||||
void markChanged() {
|
||||
externalOutputChanged.set(true);
|
||||
}
|
||||
|
||||
public boolean pollAndResetChanged() {
|
||||
return externalOutputChanged.getAndSet(false);
|
||||
/**
|
||||
* Get a bitmask returning which sides on the computer have changed, resetting the internal state.
|
||||
*
|
||||
* @return What sides on the computer have changed.
|
||||
*/
|
||||
public int pollAndResetChanges() {
|
||||
return externalOutputChanges.getAndSet(0);
|
||||
}
|
||||
|
||||
public boolean isBlinking() {
|
||||
return isOn() && blinking;
|
||||
if (!isOn() || !terminal.getCursorBlink()) return false;
|
||||
|
||||
var cursorX = terminal.getCursorX();
|
||||
var cursorY = terminal.getCursorY();
|
||||
return cursorX >= 0 && cursorX < terminal.getWidth() && cursorY >= 0 && cursorY < terminal.getHeight();
|
||||
}
|
||||
|
||||
public void addApi(ILuaAPI api) {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user