1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-11-14 13:54:52 +00:00

Merge branch 'mc-1.20.x' into mc-1.20.y

This commit is contained in:
Jonathan Coates 2024-03-22 21:23:49 +00:00
commit da5885ef35
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
134 changed files with 3783 additions and 1267 deletions

1
.gitignore vendored
View File

@ -9,6 +9,7 @@
/projects/*/logs /projects/*/logs
/projects/fabric/fabricloader.log /projects/fabric/fabricloader.log
/projects/*/build /projects/*/build
/projects/*/src/test/generated_tests/
/buildSrc/build /buildSrc/build
/out /out
/buildSrc/out /buildSrc/out

View File

@ -102,6 +102,8 @@ sourceSets.all {
option("NullAway:CastToNonNullMethod", "dan200.computercraft.core.util.Nullability.assertNonNull") option("NullAway:CastToNonNullMethod", "dan200.computercraft.core.util.Nullability.assertNonNull")
option("NullAway:CheckOptionalEmptiness") option("NullAway:CheckOptionalEmptiness")
option("NullAway:AcknowledgeRestrictiveAnnotations") option("NullAway:AcknowledgeRestrictiveAnnotations")
excludedPaths = ".*/jmh_generated/.*"
} }
} }
} }

View File

@ -32,7 +32,7 @@ val publishCurseForge by tasks.registering(TaskPublishCurseForge::class) {
apiToken = findProperty("curseForgeApiKey") ?: "" apiToken = findProperty("curseForgeApiKey") ?: ""
enabled = apiToken != "" enabled = apiToken != ""
val mainFile = upload("282001", modPublishing.output.get().archiveFile) val mainFile = upload("282001", modPublishing.output)
mainFile.changelog = mainFile.changelog =
"Release notes can be found on the [GitHub repository](https://github.com/cc-tweaked/CC-Tweaked/releases/tag/v$mcVersion-$modVersion)." "Release notes can be found on the [GitHub repository](https://github.com/cc-tweaked/CC-Tweaked/releases/tag/v$mcVersion-$modVersion)."
mainFile.changelogType = "markdown" mainFile.changelogType = "markdown"

View File

@ -13,8 +13,12 @@ SPDX-License-Identifier: MPL-2.0
<property name="tabWidth" value="4"/> <property name="tabWidth" value="4"/>
<property name="charset" value="UTF-8" /> <property name="charset" value="UTF-8" />
<module name="BeforeExecutionExclusionFileFilter">
<property name="fileNamePattern" value="module\-info\.java$"/>
</module>
<module name="SuppressionFilter"> <module name="SuppressionFilter">
<property name="file" value="${config_loc}/suppressions.xml" /> <property name="file" value="${config_loc}/suppressions.xml" />
</module> </module>
<module name="BeforeExecutionExclusionFileFilter"> <module name="BeforeExecutionExclusionFileFilter">

View File

@ -21,5 +21,5 @@ SPDX-License-Identifier: MPL-2.0
<suppress checks="PackageName" files=".*[\\/]T[A-Za-z]+.java" /> <suppress checks="PackageName" files=".*[\\/]T[A-Za-z]+.java" />
<!-- Allow underscores in our test classes. --> <!-- Allow underscores in our test classes. -->
<suppress checks="MethodName" files=".*Contract.java" /> <suppress checks="MethodName" files=".*(Contract|Test).java" />
</suppressions> </suppressions>

View File

@ -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. constellation is redundant, but it does not cause problems.
## Building a GPS constellation ## 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 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 modems or 4 ender modems. Try not to mix ender and wireless modems together as you might get some odd behavior when your

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

140
doc/reference/command.md Normal file
View 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"

View File

@ -10,7 +10,7 @@ kotlin.jvm.target.validation.mode=error
# Mod properties # Mod properties
isUnstable=true 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 # Minecraft properties: We want to configure this here so we can read it in settings.gradle
mcVersion=1.20.4 mcVersion=1.20.4

View File

@ -9,7 +9,7 @@
# Remember to update corresponding versions in fabric.mod.json/mods.toml # Remember to update corresponding versions in fabric.mod.json/mods.toml
fabric-api = "0.93.1+1.20.4" fabric-api = "0.93.1+1.20.4"
fabric-loader = "0.15.3" fabric-loader = "0.15.3"
neoForge = "20.4.161-beta" neoForge = "20.4.210"
neoForgeSpi = "8.0.1" neoForgeSpi = "8.0.1"
mixin = "0.8.5" mixin = "0.8.5"
parchment = "2023.12.31" parchment = "2023.12.31"
@ -26,7 +26,7 @@ slf4j = "2.0.7"
asm = "9.6" asm = "9.6"
autoService = "1.1.1" autoService = "1.1.1"
checkerFramework = "3.42.0" checkerFramework = "3.42.0"
cobalt = "0.9.1" cobalt = "0.9.2"
commonsCli = "1.6.0" commonsCli = "1.6.0"
jetbrainsAnnotations = "24.1.0" jetbrainsAnnotations = "24.1.0"
jsr305 = "3.0.2" jsr305 = "3.0.2"
@ -51,10 +51,11 @@ sodium = "mc1.20-0.4.10"
hamcrest = "2.2" hamcrest = "2.2"
jqwik = "1.8.2" jqwik = "1.8.2"
junit = "5.10.1" junit = "5.10.1"
jmh = "1.37"
# Build tools # Build tools
cctJavadoc = "1.8.2" cctJavadoc = "1.8.2"
checkstyle = "10.12.6" checkstyle = "10.14.1"
curseForgeGradle = "1.1.18" curseForgeGradle = "1.1.18"
errorProne-core = "2.23.0" errorProne-core = "2.23.0"
errorProne-plugin = "3.1.0" errorProne-plugin = "3.1.0"
@ -62,15 +63,15 @@ fabric-loom = "1.5.7"
githubRelease = "2.5.2" githubRelease = "2.5.2"
gradleVersions = "0.50.0" gradleVersions = "0.50.0"
ideaExt = "1.1.7" ideaExt = "1.1.7"
illuaminate = "0.1.0-44-g9ee0055" illuaminate = "0.1.0-69-gf294ab2"
lwjgl = "3.3.3" lwjgl = "3.3.3"
minotaur = "2.8.7" minotaur = "2.8.7"
neoGradle = "7.0.93" neoGradle = "7.0.100"
nullAway = "0.9.9" nullAway = "0.9.9"
spotless = "6.23.3" spotless = "6.23.3"
taskTree = "2.1.1" taskTree = "2.1.1"
teavm = "0.10.0-SQUID.2" teavm = "0.10.0-SQUID.3"
vanillaExtract = "0.1.1" vanillaExtract = "0.1.2"
versionCatalogUpdate = "0.8.1" versionCatalogUpdate = "0.8.1"
[libraries] [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-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }
junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", 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" } 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
lwjgl-bom = { module = "org.lwjgl:lwjgl-bom", version.ref = "lwjgl" } lwjgl-bom = { module = "org.lwjgl:lwjgl-bom", version.ref = "lwjgl" }

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

20
gradlew.bat vendored
View File

@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail

View File

@ -5,6 +5,7 @@
package dan200.computercraft.api.network.wired; package dan200.computercraft.api.network.wired;
import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.IPeripheral;
import org.jetbrains.annotations.ApiStatus;
import java.util.Map; import java.util.Map;
@ -22,6 +23,7 @@ import java.util.Map;
* *
* @see WiredNode#getNetwork() * @see WiredNode#getNetwork()
*/ */
@ApiStatus.NonExtendable
public interface WiredNetwork { public interface WiredNetwork {
/** /**
* Create a connection between two nodes. * Create a connection between two nodes.
@ -35,7 +37,9 @@ public interface WiredNetwork {
* @throws IllegalArgumentException If {@code left} and {@code right} are equal. * @throws IllegalArgumentException If {@code left} and {@code right} are equal.
* @see WiredNode#connectTo(WiredNode) * @see WiredNode#connectTo(WiredNode)
* @see WiredNetwork#connect(WiredNode, WiredNode) * @see WiredNetwork#connect(WiredNode, WiredNode)
* @deprecated Use {@link WiredNode#connectTo(WiredNode)}
*/ */
@Deprecated
boolean connect(WiredNode left, WiredNode right); boolean connect(WiredNode left, WiredNode right);
/** /**
@ -50,7 +54,9 @@ public interface WiredNetwork {
* @throws IllegalArgumentException If {@code left} and {@code right} are equal. * @throws IllegalArgumentException If {@code left} and {@code right} are equal.
* @see WiredNode#disconnectFrom(WiredNode) * @see WiredNode#disconnectFrom(WiredNode)
* @see WiredNetwork#connect(WiredNode, WiredNode) * @see WiredNetwork#connect(WiredNode, WiredNode)
* @deprecated Use {@link WiredNode#disconnectFrom(WiredNode)}
*/ */
@Deprecated
boolean disconnect(WiredNode left, WiredNode right); boolean disconnect(WiredNode left, WiredNode right);
/** /**
@ -64,7 +70,9 @@ public interface WiredNetwork {
* only element. * only element.
* @throws IllegalArgumentException If the node is not in the network. * @throws IllegalArgumentException If the node is not in the network.
* @see WiredNode#remove() * @see WiredNode#remove()
* @deprecated Use {@link WiredNode#remove()}
*/ */
@Deprecated
boolean remove(WiredNode node); boolean remove(WiredNode node);
/** /**
@ -77,6 +85,8 @@ public interface WiredNetwork {
* @param peripherals The new peripherals for this node. * @param peripherals The new peripherals for this node.
* @throws IllegalArgumentException If the node is not in the network. * @throws IllegalArgumentException If the node is not in the network.
* @see WiredNode#updatePeripherals(Map) * @see WiredNode#updatePeripherals(Map)
* @deprecated Use {@link WiredNode#updatePeripherals(Map)}
*/ */
@Deprecated
void updatePeripherals(WiredNode node, Map<String, IPeripheral> peripherals); void updatePeripherals(WiredNode node, Map<String, IPeripheral> peripherals);
} }

View File

@ -6,6 +6,7 @@ package dan200.computercraft.api.network.wired;
import dan200.computercraft.api.network.PacketNetwork; import dan200.computercraft.api.network.PacketNetwork;
import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.IPeripheral;
import org.jetbrains.annotations.ApiStatus;
import java.util.Map; 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 * Wired nodes also provide several convenience methods for interacting with a wired network. These should only ever
* be used on the main server thread. * be used on the main server thread.
*/ */
@ApiStatus.NonExtendable
public interface WiredNode extends PacketNetwork { public interface WiredNode extends PacketNetwork {
/** /**
* The associated element for this network node. * 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. * This should only be used on the server thread.
* *
* @return This node's network. * @return This node's network.
* @deprecated Use the connect/disconnect/remove methods on {@link WiredNode}.
*/ */
@Deprecated
WiredNetwork getNetwork(); WiredNetwork getNetwork();
/** /**
@ -47,12 +51,9 @@ public interface WiredNode extends PacketNetwork {
* *
* @param node The other node to connect to. * @param node The other node to connect to.
* @return {@code true} if a connection was created or {@code false} if the connection already exists. * @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) * @see WiredNode#disconnectFrom(WiredNode)
*/ */
default boolean connectTo(WiredNode node) { boolean connectTo(WiredNode node);
return getNetwork().connect(this, node);
}
/** /**
* Destroy a connection between this node and another. * 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. * @param node The other node to disconnect from.
* @return {@code true} if a connection was destroyed or {@code false} if no connection exists. * @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) * @see WiredNode#connectTo(WiredNode)
*/ */
default boolean disconnectFrom(WiredNode node) { boolean disconnectFrom(WiredNode node);
return getNetwork().disconnect(this, node);
}
/** /**
* Sever all connections this node has, removing it from this network. * 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 * @return Whether this node was removed from the network. One cannot remove a node from a network where it is the
* only element. * only element.
* @throws IllegalArgumentException If the node is not in the network. * @throws IllegalArgumentException If the node is not in the network.
* @see WiredNetwork#remove(WiredNode)
*/ */
default boolean remove() { boolean remove();
return getNetwork().remove(this);
}
/** /**
* Mark this node's peripherals as having changed. * Mark this node's peripherals as having changed.
@ -91,9 +85,6 @@ public interface WiredNode extends PacketNetwork {
* that your network element owns. * that your network element owns.
* *
* @param peripherals The new peripherals for this node. * @param peripherals The new peripherals for this node.
* @see WiredNetwork#updatePeripherals(WiredNode, Map)
*/ */
default void updatePeripherals(Map<String, IPeripheral> peripherals) { void updatePeripherals(Map<String, IPeripheral> peripherals);
getNetwork().updatePeripherals(this, peripherals);
}
} }

View File

@ -29,7 +29,7 @@ public final class Services {
* @throws IllegalStateException When the service cannot be loaded. * @throws IllegalStateException When the service cannot be loaded.
*/ */
public static <T> T load(Class<T> klass) { 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()) { return switch (services.size()) {
case 1 -> services.get(0).get(); case 1 -> services.get(0).get();
case 0 -> throw new IllegalStateException("Cannot find service for " + klass.getName()); case 0 -> throw new IllegalStateException("Cannot find service for " + klass.getName());

View File

@ -46,6 +46,9 @@ dependencies {
testImplementation(libs.bundles.test) testImplementation(libs.bundles.test)
testRuntimeOnly(libs.bundles.testRuntime) testRuntimeOnly(libs.bundles.testRuntime)
testImplementation(libs.jmh)
testAnnotationProcessor(libs.jmh.processor)
testModCompileOnly(libs.mixin) testModCompileOnly(libs.mixin)
testModImplementation(testFixtures(project(":core"))) testModImplementation(testFixtures(project(":core")))
testModImplementation(testFixtures(project(":common"))) testModImplementation(testFixtures(project(":common")))

View File

@ -22,6 +22,7 @@ import dan200.computercraft.core.util.Colour;
import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.command.CommandComputerCraft; import dan200.computercraft.shared.command.CommandComputerCraft;
import dan200.computercraft.shared.common.IColouredItem; 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.core.ServerContext;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import dan200.computercraft.shared.computer.inventory.ViewComputerMenu; 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. * Register any client-side objects which must be done on the main thread.
*
* @param itemProperties Callback to register item properties.
*/ */
public static void registerMainThread() { public static void registerMainThread(RegisterItemProperty itemProperties) {
registerItemProperty("state", registerItemProperty(itemProperties, "state",
new UnclampedPropertyFunction((stack, world, player, random) -> ClientPocketComputers.get(stack).getState().ordinal()), 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 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, (stack, world, player, random) -> IColouredItem.getColourBasic(stack) != -1 ? 1 : 0,
ModRegistry.Items.POCKET_COMPUTER_NORMAL, ModRegistry.Items.POCKET_COMPUTER_ADVANCED ModRegistry.Items.POCKET_COMPUTER_NORMAL, ModRegistry.Items.POCKET_COMPUTER_ADVANCED
); );
@ -125,9 +131,17 @@ public final class ClientRegistry {
} }
@SafeVarargs @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); 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) { 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) { private static int getPocketColour(ItemStack stack, int layer) {
switch (layer) { return switch (layer) {
case 0: default -> 0xFFFFFF;
default: case 1 -> IColouredItem.getColourBasic(stack); // Frame colour
return 0xFFFFFF; case 2 -> { // Light colour
case 1: // Frame colour var computer = ClientPocketComputers.get(stack);
return IColouredItem.getColourBasic(stack); yield computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState();
case 2: { // Light colour
var light = ClientPocketComputers.get(stack).getLightState();
return light == -1 ? Colour.BLACK.getHex() : light;
} }
} };
} }
private static int getTurtleColour(ItemStack stack, int layer) { private static int getTurtleColour(ItemStack stack, int layer) {

View File

@ -17,6 +17,7 @@ import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.computer.upload.UploadResult; import dan200.computercraft.shared.computer.upload.UploadResult;
import dan200.computercraft.shared.network.client.ClientNetworkContext; import dan200.computercraft.shared.network.client.ClientNetworkContext;
import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity; import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity;
import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition; import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
@ -27,7 +28,6 @@ import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.UUID; import java.util.UUID;
/** /**
@ -67,19 +67,17 @@ public final class ClientNetworkContextImpl implements ClientNetworkContext {
} }
@Override @Override
public void handlePocketComputerData(int instanceId, ComputerState state, int lightState, TerminalState terminal) { public void handlePocketComputerData(UUID instanceId, ComputerState state, int lightState, TerminalState terminal) {
var computer = ClientPocketComputers.get(instanceId, terminal.colour); ClientPocketComputers.setState(instanceId, state, lightState, terminal);
computer.setState(state, lightState);
if (terminal.hasTerminal()) computer.setTerminal(terminal);
} }
@Override @Override
public void handlePocketComputerDeleted(int instanceId) { public void handlePocketComputerDeleted(UUID instanceId) {
ClientPocketComputers.remove(instanceId); ClientPocketComputers.remove(instanceId);
} }
@Override @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); SpeakerManager.getSound(source).playAudio(reifyPosition(position), volume, buffer);
} }

View File

@ -4,21 +4,26 @@
package dan200.computercraft.client.pocket; 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.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.network.client.PocketComputerDataMessage;
import dan200.computercraft.shared.pocket.items.PocketComputerItem; 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 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> * <p>
* This is populated by {@link PocketComputerDataMessage} and accessed when rendering pocket computers * This is populated by {@link PocketComputerDataMessage} and accessed when rendering pocket computers
*/ */
public final class ClientPocketComputers { public final class ClientPocketComputers {
private static final Int2ObjectMap<PocketComputerData> instances = new Int2ObjectOpenHashMap<>(); private static final Map<UUID, PocketComputerData> instances = new HashMap<>();
private ClientPocketComputers() { private ClientPocketComputers() {
} }
@ -27,25 +32,32 @@ public final class ClientPocketComputers {
instances.clear(); instances.clear();
} }
public static void remove(int id) { public static void remove(UUID id) {
instances.remove(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 instanceId The instance ID of the pocket computer.
* @param advanced Whether this computer has an advanced terminal. * @param state The computer state of the pocket computer.
* @return The pocket computer data. * @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); var computer = instances.get(instanceId);
if (computer == null) instances.put(instanceId, computer = new PocketComputerData(advanced)); if (computer == null) {
return computer; 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) { public static @Nullable PocketComputerData get(ItemStack stack) {
var family = stack.getItem() instanceof PocketComputerItem computer ? computer.getFamily() : ComputerFamily.NORMAL; var id = PocketComputerItem.getInstanceID(stack);
return get(PocketComputerItem.getInstanceID(stack), family != ComputerFamily.NORMAL); return id == null ? null : instances.get(id);
} }
} }

View File

@ -4,11 +4,8 @@
package dan200.computercraft.client.pocket; package dan200.computercraft.client.pocket;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.shared.computer.core.ComputerState; import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.terminal.NetworkedTerminal; 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; 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 ClientPocketComputers The registry which holds pocket computers.
* @see PocketServerComputer The server-side pocket computer. * @see PocketServerComputer The server-side pocket computer.
*/ */
public class PocketComputerData { public final class PocketComputerData {
private final NetworkedTerminal terminal; private final NetworkedTerminal terminal;
private ComputerState state = ComputerState.OFF; private ComputerState state;
private int lightColour = -1; private int lightColour;
public PocketComputerData(boolean colour) { PocketComputerData(ComputerState state, int lightColour, NetworkedTerminal terminal) {
terminal = new NetworkedTerminal(Config.pocketTermWidth, Config.pocketTermHeight, colour); this.state = state;
this.lightColour = lightColour;
this.terminal = terminal;
} }
public int getLightState() { public int getLightState() {
return state != ComputerState.OFF ? lightColour : -1; return state != ComputerState.OFF ? lightColour : -1;
} }
public Terminal getTerminal() { public NetworkedTerminal getTerminal() {
return terminal; return terminal;
} }
@ -42,12 +41,8 @@ public class PocketComputerData {
return state; return state;
} }
public void setState(ComputerState state, int lightColour) { void setState(ComputerState state, int lightColour) {
this.state = state; this.state = state;
this.lightColour = lightColour; this.lightColour = lightColour;
} }
public void setTerminal(TerminalState state) {
state.apply(terminal);
}
} }

View File

@ -11,6 +11,7 @@ import dan200.computercraft.client.pocket.ClientPocketComputers;
import dan200.computercraft.client.render.text.FixedWidthFontRenderer; import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
import dan200.computercraft.core.util.Colour; import dan200.computercraft.core.util.Colour;
import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.pocket.items.PocketComputerItem; import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
@ -32,10 +33,16 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
@Override @Override
protected void renderItem(PoseStack transform, MultiBufferSource bufferSource, ItemStack stack, int light) { protected void renderItem(PoseStack transform, MultiBufferSource bufferSource, ItemStack stack, int light) {
var computer = ClientPocketComputers.get(stack); var computer = ClientPocketComputers.get(stack);
var terminal = computer.getTerminal(); var terminal = computer == null ? null : computer.getTerminal();
var termWidth = terminal.getWidth(); int termWidth, termHeight;
var termHeight = terminal.getHeight(); if (terminal == null) {
termWidth = Config.pocketTermWidth;
termHeight = Config.pocketTermHeight;
} else {
termWidth = terminal.getWidth();
termHeight = terminal.getHeight();
}
var width = termWidth * FONT_WIDTH + MARGIN * 2; var width = termWidth * FONT_WIDTH + MARGIN * 2;
var height = termHeight * FONT_HEIGHT + 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); renderFrame(matrix, bufferSource, family, frameColour, light, width, height);
// Render the light // Render the light
var lightColour = ClientPocketComputers.get(stack).getLightState(); var lightColour = computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState();
if (lightColour == -1) lightColour = Colour.BLACK.getHex();
renderLight(transform, bufferSource, lightColour, width, height); renderLight(transform, bufferSource, lightColour, width, height);
FixedWidthFontRenderer.drawTerminal( var quadEmitter = FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL));
FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL)), if (terminal == null) {
MARGIN, MARGIN, terminal, MARGIN, MARGIN, MARGIN, MARGIN FixedWidthFontRenderer.drawEmptyTerminal(quadEmitter, 0, 0, width, height);
); } else {
FixedWidthFontRenderer.drawTerminal(quadEmitter, MARGIN, MARGIN, terminal, MARGIN, MARGIN, MARGIN, MARGIN);
}
transform.popPose(); transform.popPose();
} }

View File

@ -60,9 +60,9 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
@Override @Override
public void render(MonitorBlockEntity monitor, float partialTicks, PoseStack transform, MultiBufferSource bufferSource, int lightmapCoord, int overlayLight) { public void render(MonitorBlockEntity monitor, float partialTicks, PoseStack transform, MultiBufferSource bufferSource, int lightmapCoord, int overlayLight) {
// Render from the origin monitor // Render from the origin monitor
var originTerminal = monitor.getClientMonitor(); var originTerminal = monitor.getOriginClientMonitor();
if (originTerminal == null) return; if (originTerminal == null) return;
var origin = originTerminal.getOrigin(); var origin = originTerminal.getOrigin();
var renderState = originTerminal.getRenderState(MonitorRenderState::new); var renderState = originTerminal.getRenderState(MonitorRenderState::new);
var monitorPos = monitor.getBlockPos(); var monitorPos = monitor.getBlockPos();

View File

@ -5,6 +5,7 @@
package dan200.computercraft.client.sound; package dan200.computercraft.client.sound;
import com.mojang.blaze3d.audio.Channel; 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.SpeakerPeripheral;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition; import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.client.sounds.AudioStream; import net.minecraft.client.sounds.AudioStream;
@ -36,7 +37,7 @@ class DfpwmStream implements AudioStream {
/** /**
* The {@link Channel} which this sound is playing on. * The {@link Channel} which this sound is playing on.
* *
* @see SpeakerInstance#playAudio(SpeakerPosition, float, ByteBuffer) * @see SpeakerInstance#playAudio(SpeakerPosition, float, EncodedAudio)
*/ */
@Nullable @Nullable
Channel channel; Channel channel;
@ -44,21 +45,23 @@ class DfpwmStream implements AudioStream {
/** /**
* The underlying {@link SoundEngine} executor. * The underlying {@link SoundEngine} executor.
* *
* @see SpeakerInstance#playAudio(SpeakerPosition, float, ByteBuffer) * @see SpeakerInstance#playAudio(SpeakerPosition, float, EncodedAudio)
* @see SoundEngine#executor * @see SoundEngine#executor
*/ */
@Nullable @Nullable
Executor executor; Executor executor;
private int charge = 0; // q
private int strength = 0; // s
private int lowPassCharge; private int lowPassCharge;
private boolean previousBit = false;
DfpwmStream() { 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 readable = input.remaining();
var output = ByteBuffer.allocate(readable * 8).order(ByteOrder.nativeOrder()); var output = ByteBuffer.allocate(readable * 8).order(ByteOrder.nativeOrder());

View File

@ -6,12 +6,12 @@ package dan200.computercraft.client.sound;
import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.core.util.Nullability; import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition; import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import javax.annotation.Nullable; 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. * 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() { SpeakerInstance() {
} }
private void pushAudio(ByteBuffer buffer) { private void pushAudio(EncodedAudio buffer) {
var sound = this.sound; var sound = this.sound;
var stream = currentStream; 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); pushAudio(buffer);
var soundManager = Minecraft.getInstance().getSoundManager(); var soundManager = Minecraft.getInstance().getSoundManager();

View File

@ -13,6 +13,7 @@ import dan200.computercraft.api.upgrades.UpgradeBase;
import dan200.computercraft.core.metrics.Metric; import dan200.computercraft.core.metrics.Metric;
import dan200.computercraft.core.metrics.Metrics; import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.shared.ModRegistry; 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.Aggregate;
import dan200.computercraft.shared.computer.metrics.basic.AggregatedMetric; import dan200.computercraft.shared.computer.metrics.basic.AggregatedMetric;
import dan200.computercraft.shared.config.ConfigFile; 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.exception", "Unhandled exception (%s)");
add("commands.computercraft.generic.additional_rows", "%d additional rows…"); 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.no_matching", "No computers matching '%s'");
add("argument.computercraft.computer.many_matching", "Multiple computers matching '%s' (instances %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.tracking_field.no_field", "Unknown field '%s'");
add("argument.computercraft.argument_expected", "Argument expected"); add("argument.computercraft.argument_expected", "Argument expected");
add("argument.computercraft.unknown_computer_family", "Unknown computer family '%s'");
// Metrics // Metrics
add(Metrics.COMPUTER_TASKS, "Tasks"); add(Metrics.COMPUTER_TASKS, "Tasks");
@ -281,7 +291,8 @@ public final class LanguageProvider implements DataProvider {
pocketUpgrades.getGeneratedUpgrades().stream().map(UpgradeBase::getUnlocalisedAdjective), pocketUpgrades.getGeneratedUpgrades().stream().map(UpgradeBase::getUnlocalisedAdjective),
Metric.metrics().values().stream().map(x -> AggregatedMetric.TRANSLATION_PREFIX + x.name() + ".name"), Metric.metrics().values().stream().map(x -> AggregatedMetric.TRANSLATION_PREFIX + x.name() + ".name"),
ConfigSpec.serverSpec.entries().map(ConfigFile.Entry::translationKey), 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); ).flatMap(x -> x);
} }

View File

@ -4,45 +4,66 @@
package dan200.computercraft.impl.network.wired; package dan200.computercraft.impl.network.wired;
import org.jetbrains.annotations.Contract;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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> * <p>
* This adds substantial overhead to network modification, and so should only be enabled * This adds substantial overhead to network modification, and so is only enabled when assertions are enabled.
* in a development environment.
*/ */
public final class InvariantChecker { final class InvariantChecker {
private static final Logger LOG = LoggerFactory.getLogger(InvariantChecker.class); private static final Logger LOG = LoggerFactory.getLogger(InvariantChecker.class);
private static final boolean ENABLED = false;
private InvariantChecker() { private InvariantChecker() {
} }
public static void checkNode(WiredNodeImpl node) { static void checkNode(WiredNodeImpl node) {
if (!ENABLED) return; assert checkNodeImpl(node) : "Node invariants failed. See logs.";
}
var network = node.network; private static boolean checkNodeImpl(WiredNodeImpl node) {
if (network == null) { var okay = true;
LOG.error("Node's network is null", new Exception());
return; if (node.currentSet != null) {
okay = false;
LOG.error("{}: currentSet was not cleared.", node);
} }
if (network.nodes == null || !network.nodes.contains(node)) { var network = makeNullable(node.network);
LOG.error("Node's network does not contain node", new Exception()); 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) { for (var neighbour : node.neighbours) {
if (!neighbour.neighbours.contains(node)) { 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) { static void checkNetwork(WiredNetworkImpl network) {
if (!ENABLED) return; 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;
} }
} }

View File

@ -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);
}
}

View File

@ -6,6 +6,7 @@ package dan200.computercraft.impl.network.wired;
import dan200.computercraft.api.network.wired.WiredNetworkChange; import dan200.computercraft.api.network.wired.WiredNetworkChange;
import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.core.util.PeripheralHelpers;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -52,7 +53,7 @@ final class WiredNetworkChangeImpl implements WiredNetworkChange {
var oldValue = entry.getValue(); var oldValue = entry.getValue();
if (newPeripherals.containsKey(oldKey)) { if (newPeripherals.containsKey(oldKey)) {
var rightValue = added.get(oldKey); var rightValue = added.get(oldKey);
if (oldValue.equals(rightValue)) { if (PeripheralHelpers.equals(oldValue, rightValue)) {
added.remove(oldKey); added.remove(oldKey);
} else { } else {
removed.put(oldKey, oldValue); removed.put(oldKey, oldValue);

View File

@ -8,6 +8,7 @@ import dan200.computercraft.api.network.Packet;
import dan200.computercraft.api.network.wired.WiredNetwork; import dan200.computercraft.api.network.wired.WiredNetwork;
import dan200.computercraft.api.network.wired.WiredNode; import dan200.computercraft.api.network.wired.WiredNode;
import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.core.util.Nullability;
import java.util.*; import java.util.*;
import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReadWriteLock;
@ -187,10 +188,76 @@ final class WiredNetworkImpl implements WiredNetwork {
return true; 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 // Broadcast our simple peripheral changes
removeSingleNode(wired, wiredNetwork); removeSingleNode(wired, wiredNetwork);
InvariantChecker.checkNode(wired); InvariantChecker.checkNode(wired);
@ -198,43 +265,46 @@ final class WiredNetworkImpl implements WiredNetwork {
return true; return true;
} }
// A split may cause 2..neighbours.size() separate networks, so we assert queue.size() == nodes.size() : "Expected queue to contain all nodes.";
// 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));
while (!neighbours.isEmpty()) { // Otherwise we need to create our new networks.
reachable = reachableNodes(neighbours.iterator().next()); var networks = new ArrayList<WiredNetworkImpl>(1 + nodeSets.size());
neighbours.removeAll(reachable); // Add the network we've created for the removed node.
maximals.add(new WiredNetworkImpl(reachable)); 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 { try {
// We special case the original node: detaching all peripherals when needed. // We special case the original node: detaching all peripherals when needed.
wired.network = wiredNetwork; wired.network = wiredNetwork;
wired.peripherals = Map.of(); wired.peripherals = Map.of();
wired.neighbours.clear();
// Ensure every network is finalised // Add all nodes to their appropriate network.
for (var network : maximals) { for (var child : queue) {
for (var child : network.nodes) { var network = Nullability.assertNonNull(child.currentSet).network();
child.network = network; child.currentSet = null;
network.peripherals.putAll(child.peripherals);
} 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); InvariantChecker.checkNode(wired);
// Then broadcast network changes once all nodes are finalised // 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); WiredNetworkChangeImpl.changeOf(peripherals, network.peripherals).broadcast(network.nodes);
} }
} finally { } finally {
for (var network : maximals) network.lock.writeLock().unlock(); for (var network : networks) network.lock.writeLock().unlock();
} }
nodes.clear(); nodes.clear();
@ -373,22 +443,4 @@ final class WiredNetworkImpl implements WiredNetwork {
throw new IllegalArgumentException("Unknown implementation of IWiredNode: " + node); 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;
}
} }

View File

@ -27,11 +27,39 @@ public final class WiredNodeImpl implements WiredNode {
final HashSet<WiredNodeImpl> neighbours = new HashSet<>(); final HashSet<WiredNodeImpl> neighbours = new HashSet<>();
volatile WiredNetworkImpl network; volatile WiredNetworkImpl network;
/**
* A temporary field used when checking network connectivity.
*
* @see WiredNetworkImpl#remove(WiredNode)
*/
@Nullable
NodeSet currentSet;
public WiredNodeImpl(WiredElement element) { public WiredNodeImpl(WiredElement element) {
this.element = element; this.element = element;
network = new WiredNetworkImpl(this); 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 @Override
public synchronized void addReceiver(PacketReceiver receiver) { public synchronized void addReceiver(PacketReceiver receiver) {
if (receivers == null) receivers = new HashSet<>(); if (receivers == null) receivers = new HashSet<>();

View File

@ -18,6 +18,7 @@ import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.server.dedicated.DedicatedServer; import net.minecraft.server.dedicated.DedicatedServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.packs.resources.PreparableReloadListener; import net.minecraft.server.packs.resources.PreparableReloadListener;
import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.Entity;
@ -78,10 +79,19 @@ public final class CommonHooks {
NetworkUtils.reset(); 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) { public static void onChunkWatch(LevelChunk chunk, ServerPlayer player) {
MonitorWatcher.onWatch(chunk, 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"); 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( private static final Set<ResourceLocation> TREASURE_DISK_LOOT_TABLES = Set.of(

View File

@ -19,7 +19,6 @@ import dan200.computercraft.impl.PocketUpgrades;
import dan200.computercraft.impl.TurtleUpgrades; import dan200.computercraft.impl.TurtleUpgrades;
import dan200.computercraft.shared.command.UserLevel; import dan200.computercraft.shared.command.UserLevel;
import dan200.computercraft.shared.command.arguments.ComputerArgumentType; 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.RepeatArgumentType;
import dan200.computercraft.shared.command.arguments.TrackingFieldArgumentType; import dan200.computercraft.shared.command.arguments.TrackingFieldArgumentType;
import dan200.computercraft.shared.common.ClearColourRecipe; import dan200.computercraft.shared.common.ClearColourRecipe;
@ -338,8 +337,7 @@ public final class ModRegistry {
static { static {
register("tracking_field", TrackingFieldArgumentType.class, TrackingFieldArgumentType.metric()); register("tracking_field", TrackingFieldArgumentType.class, TrackingFieldArgumentType.metric());
register("computer", ComputerArgumentType.class, ComputerArgumentType.oneComputer()); register("computer", ComputerArgumentType.class, ComputerArgumentType.get());
register("computers", ComputersArgumentType.class, new ComputersArgumentType.Info());
registerUnsafe("repeat", RepeatArgumentType.class, new RepeatArgumentType.Info()); registerUnsafe("repeat", RepeatArgumentType.class, new RepeatArgumentType.Info());
} }
} }

View File

@ -12,7 +12,8 @@ import com.mojang.brigadier.suggestion.Suggestions;
import dan200.computercraft.core.computer.ComputerSide; import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.metrics.Metrics; import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.shared.ModRegistry; 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.command.text.TableBuilder;
import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer; 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.BasicComputerMetricsObserver;
import dan200.computercraft.shared.computer.metrics.basic.ComputerMetrics; import dan200.computercraft.shared.computer.metrics.basic.ComputerMetrics;
import dan200.computercraft.shared.network.container.ComputerContainerData; import dan200.computercraft.shared.network.container.ComputerContainerData;
import net.minecraft.ChatFormatting;
import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.CommandSourceStack;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
@ -42,9 +44,6 @@ import java.util.*;
import static dan200.computercraft.shared.command.CommandUtils.isPlayer; import static dan200.computercraft.shared.command.CommandUtils.isPlayer;
import static dan200.computercraft.shared.command.Exceptions.NOT_TRACKING_EXCEPTION; 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.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.arguments.TrackingFieldArgumentType.metric;
import static dan200.computercraft.shared.command.builder.CommandBuilder.args; import static dan200.computercraft.shared.command.builder.CommandBuilder.args;
import static dan200.computercraft.shared.command.builder.CommandBuilder.command; import static dan200.computercraft.shared.command.builder.CommandBuilder.command;
@ -70,37 +69,37 @@ public final class CommandComputerCraft {
.requires(ModRegistry.Permissions.PERMISSION_DUMP) .requires(ModRegistry.Permissions.PERMISSION_DUMP)
.executes(c -> dump(c.getSource())) .executes(c -> dump(c.getSource()))
.then(args() .then(args()
.arg("computer", oneComputer()) .arg("computer", ComputerArgumentType.get())
.executes(c -> dumpComputer(c.getSource(), getComputerArgument(c, "computer"))))) .executes(c -> dumpComputer(c.getSource(), ComputerArgumentType.getOne(c, "computer")))))
.then(command("shutdown") .then(command("shutdown")
.requires(ModRegistry.Permissions.PERMISSION_SHUTDOWN) .requires(ModRegistry.Permissions.PERMISSION_SHUTDOWN)
.argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers()) .argManyValue("computers", ComputerArgumentType.get(), ComputerSelector.all())
.executes((c, a) -> shutdown(c.getSource(), unwrap(c.getSource(), a)))) .executes((c, a) -> shutdown(c.getSource(), unwrap(c.getSource(), a))))
.then(command("turn-on") .then(command("turn-on")
.requires(ModRegistry.Permissions.PERMISSION_TURN_ON) .requires(ModRegistry.Permissions.PERMISSION_TURN_ON)
.argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers()) .argManyValue("computers", ComputerArgumentType.get(), ComputerSelector.all())
.executes((c, a) -> turnOn(c.getSource(), unwrap(c.getSource(), a)))) .executes((c, a) -> turnOn(c.getSource(), unwrap(c.getSource(), a))))
.then(command("tp") .then(command("tp")
.requires(ModRegistry.Permissions.PERMISSION_TP) .requires(ModRegistry.Permissions.PERMISSION_TP)
.arg("computer", oneComputer()) .arg("computer", ComputerArgumentType.get())
.executes(c -> teleport(c.getSource(), getComputerArgument(c, "computer")))) .executes(c -> teleport(c.getSource(), ComputerArgumentType.getOne(c, "computer"))))
.then(command("queue") .then(command("queue")
.requires(ModRegistry.Permissions.PERMISSION_QUEUE) .requires(ModRegistry.Permissions.PERMISSION_QUEUE)
.arg( .arg(
RequiredArgumentBuilder.<CommandSourceStack, ComputersArgumentType.ComputersSupplier>argument("computer", manyComputers()) RequiredArgumentBuilder.<CommandSourceStack, ComputerSelector>argument("computer", ComputerArgumentType.get())
.suggests((context, builder) -> Suggestions.empty()) .suggests((context, builder) -> Suggestions.empty())
) )
.argManyValue("args", StringArgumentType.string(), List.of()) .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") .then(command("view")
.requires(ModRegistry.Permissions.PERMISSION_VIEW) .requires(ModRegistry.Permissions.PERMISSION_VIEW)
.arg("computer", oneComputer()) .arg("computer", ComputerArgumentType.get())
.executes(c -> view(c.getSource(), getComputerArgument(c, "computer")))) .executes(c -> view(c.getSource(), ComputerArgumentType.getOne(c, "computer"))))
.then(choice("track") .then(choice("track")
.requires(ModRegistry.Permissions.PERMISSION_TRACK) .requires(ModRegistry.Permissions.PERMISSION_TRACK)
@ -135,7 +134,7 @@ public final class CommandComputerCraft {
} else if (b.getLevel() == world) { } else if (b.getLevel() == world) {
return 1; return 1;
} else { } 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) { private static int dumpComputer(CommandSourceStack source, ServerComputer computer) {
var table = new TableBuilder("Dump"); 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("Id"), text(Integer.toString(computer.getID())));
table.row(header("Label"), text(computer.getLabel())); table.row(header("Label"), text(computer.getLabel()));
table.row(header("On"), bool(computer.isOn())); table.row(header("On"), bool(computer.isOn()));
@ -332,29 +332,22 @@ public final class CommandComputerCraft {
// Additional helper functions. // 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(""); var out = Component.literal("");
// Append the computer instance // And instance
if (serverComputer == null) { if (computer == null) {
out.append(text("?")); out.append("#" + computerId + " ").append(coloured("(unloaded)", ChatFormatting.GRAY));
} else { } else {
out.append(link( out.append(makeComputerDumpCommand(computer));
text(Integer.toString(serverComputer.getInstanceID())),
"/computercraft dump " + serverComputer.getInstanceID(),
Component.translatable("commands.computercraft.dump.action")
));
} }
// And ID
out.append(" (id " + computerId + ")");
// And, if we're a player, some useful links // 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)) { if (ModRegistry.Permissions.PERMISSION_TP.test(source)) {
out.append(" ").append(link( out.append(" ").append(link(
text("\u261b"), text("\u261b"),
"/computercraft tp " + serverComputer.getInstanceID(), makeComputerCommand("tp", computer),
Component.translatable("commands.computercraft.tp.action") Component.translatable("commands.computercraft.tp.action")
)); ));
} }
@ -362,7 +355,7 @@ public final class CommandComputerCraft {
if (ModRegistry.Permissions.PERMISSION_VIEW.test(source)) { if (ModRegistry.Permissions.PERMISSION_VIEW.test(source)) {
out.append(" ").append(link( out.append(" ").append(link(
text("\u20e2"), text("\u20e2"),
"/computercraft view " + serverComputer.getInstanceID(), makeComputerCommand("view", computer),
Component.translatable("commands.computercraft.view.action") Component.translatable("commands.computercraft.view.action")
)); ));
} }
@ -380,7 +373,7 @@ public final class CommandComputerCraft {
if (ModRegistry.Permissions.PERMISSION_TP.test(context)) { if (ModRegistry.Permissions.PERMISSION_TP.test(context)) {
return link( return link(
position(computer.getPosition()), position(computer.getPosition()),
"/computercraft tp " + computer.getInstanceID(), makeComputerCommand("tp", computer),
Component.translatable("commands.computercraft.tp.action") Component.translatable("commands.computercraft.tp.action")
); );
} else { } else {
@ -392,7 +385,7 @@ public final class CommandComputerCraft {
var file = new File(ServerContext.get(source.getServer()).storageDir().toFile(), "computer/" + id); var file = new File(ServerContext.get(source.getServer()).storageDir().toFile(), "computer/" + id);
if (!file.isDirectory()) return null; if (!file.isDirectory()) return null;
return link( return clientLink(
text("\u270E"), text("\u270E"),
"/" + CLIENT_OPEN_FOLDER + " " + id, "/" + CLIENT_OPEN_FOLDER + " " + id,
Component.translatable("commands.computercraft.dump.open_path") Component.translatable("commands.computercraft.dump.open_path")
@ -431,4 +424,10 @@ public final class CommandComputerCraft {
table.display(source); table.display(source);
return timings.size(); 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;
}
} }

View File

@ -28,12 +28,12 @@ public final class CommandUtils {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static CompletableFuture<Suggestions> suggestOnServer(CommandContext<?> context, Function<CommandContext<CommandSourceStack>, CompletableFuture<Suggestions>> supplier) { public static CompletableFuture<Suggestions> suggestOnServer(CommandContext<?> context, Function<CommandContext<CommandSourceStack>, CompletableFuture<Suggestions>> supplier) {
var source = context.getSource(); var source = context.getSource();
if (!(source instanceof SharedSuggestionProvider)) { if (!(source instanceof SharedSuggestionProvider shared)) {
return Suggestions.empty(); return Suggestions.empty();
} else if (source instanceof CommandSourceStack) { } else if (source instanceof CommandSourceStack) {
return supplier.apply((CommandContext<CommandSourceStack>) context); return supplier.apply((CommandContext<CommandSourceStack>) context);
} else { } else {
return ((SharedSuggestionProvider) source).customSuggestion(context); return shared.customSuggestion(context);
} }
} }

View File

@ -7,6 +7,8 @@ package dan200.computercraft.shared.command;
import com.mojang.brigadier.exceptions.Dynamic2CommandExceptionType; import com.mojang.brigadier.exceptions.Dynamic2CommandExceptionType;
import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; 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; import net.minecraft.network.chat.Component;
public final class Exceptions { 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 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) { private static SimpleCommandExceptionType translated(String key) {
return new SimpleCommandExceptionType(Component.translatable(key)); return new SimpleCommandExceptionType(Component.translatable(key));
} }

View File

@ -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;
}
}

View File

@ -14,66 +14,58 @@ import dan200.computercraft.shared.computer.core.ServerComputer;
import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.CommandSourceStack;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import static dan200.computercraft.shared.command.Exceptions.COMPUTER_ARG_MANY; public final class ComputerArgumentType implements ArgumentType<ComputerSelector> {
public final class ComputerArgumentType implements ArgumentType<ComputerArgumentType.ComputerSupplier> {
private static final ComputerArgumentType INSTANCE = new ComputerArgumentType(); private static final ComputerArgumentType INSTANCE = new ComputerArgumentType();
public static ComputerArgumentType oneComputer() { private static final List<String> EXAMPLES = List.of(
return INSTANCE; "0", "123", "@c[instance_id=123]"
} );
public static ServerComputer getComputerArgument(CommandContext<CommandSourceStack> context, String name) throws CommandSyntaxException { public static ComputerArgumentType get() {
return context.getArgument(name, ComputerSupplier.class).unwrap(context.getSource()); return INSTANCE;
} }
private ComputerArgumentType() { 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 @Override
public ComputerSupplier parse(StringReader reader) throws CommandSyntaxException { public ComputerSelector parse(StringReader reader) throws CommandSyntaxException {
var start = reader.getCursor(); return ComputerSelector.parse(reader);
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());
};
} }
@Override @Override
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) { public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
return ComputersArgumentType.someComputers().listSuggestions(context, builder); return ComputerSelector.suggest(context, builder);
} }
@Override @Override
public Collection<String> getExamples() { public Collection<String> getExamples() {
return ComputersArgumentType.someComputers().getExamples(); return EXAMPLES;
}
@FunctionalInterface
public interface ComputerSupplier {
ServerComputer unwrap(CommandSourceStack source) throws CommandSyntaxException;
} }
} }

View File

@ -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();
});
}
}

View File

@ -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;
}
}

View File

@ -4,6 +4,8 @@
package dan200.computercraft.shared.command.text; 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.ChatFormatting;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.ClickEvent; 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); 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) { public static Component link(Component component, ClickEvent click, Component toolTip) {
var style = component.getStyle(); 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"))) .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")
);
}
} }

View File

@ -23,6 +23,7 @@ import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.LevelReader; import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.EntityBlock; 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); 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 @Nullable
@Override @Override
@Deprecated @Deprecated

View File

@ -36,13 +36,14 @@ import net.minecraft.world.level.block.state.BlockState;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
public abstract class AbstractComputerBlockEntity extends BlockEntity implements IComputerBlockEntity, Nameable, MenuProvider { public abstract class AbstractComputerBlockEntity extends BlockEntity implements IComputerBlockEntity, Nameable, MenuProvider {
private static final String NBT_ID = "ComputerId"; private static final String NBT_ID = "ComputerId";
private static final String NBT_LABEL = "Label"; private static final String NBT_LABEL = "Label";
private static final String NBT_ON = "On"; private static final String NBT_ON = "On";
private int instanceID = -1; private @Nullable UUID instanceID = null;
private int computerID = -1; private int computerID = -1;
protected @Nullable String label = null; protected @Nullable String label = null;
private boolean on = false; private boolean on = false;
@ -66,7 +67,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
var computer = getServerComputer(); var computer = getServerComputer();
if (computer != null) computer.close(); if (computer != null) computer.close();
instanceID = -1; instanceID = null;
} }
@Override @Override
@ -113,19 +114,16 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
return InteractionResult.PASS; return InteractionResult.PASS;
} }
public void neighborChanged(BlockPos neighbour) {
updateInputAt(neighbour);
}
protected void serverTick() { protected void serverTick() {
if (getLevel().isClientSide) return; if (getLevel().isClientSide) return;
if (computerID < 0 && !startOn) return; // Don't tick if we don't need a computer! if (computerID < 0 && !startOn) return; // Don't tick if we don't need a computer!
var computer = createServerComputer(); var computer = createServerComputer();
// Update any peripherals that have changed.
if (invalidSides != 0) { if (invalidSides != 0) {
for (var direction : DirectionUtil.FACINGS) { 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; fresh = false;
computerID = computer.getID(); computerID = computer.getID();
label = computer.getLabel();
on = computer.isOn();
// Update the block state if needed. We don't fire a block update intentionally, // If the on state has changed, mark as as dirty.
// as this only really is needed on the client side. 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()); updateBlockState(computer.getState());
// TODO: This should ideally be split up into label/id/on (which should save NBT and sync to client) and var changes = computer.pollAndResetChanges();
// redstone (which should update outputs) if (changes != 0) {
if (computer.hasOutputChanged()) updateOutput(); for (var direction : DirectionUtil.FACINGS) {
if ((changes & (1 << remapToLocalSide(direction).ordinal())) != 0) updateRedstoneTo(direction);
}
}
} }
protected abstract void updateBlockState(ComputerState newState); protected abstract void updateBlockState(ComputerState newState);
@ -198,11 +210,15 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
return localSide; return localSide;
} }
private void updateRedstoneInputs(ServerComputer computer) { /**
var pos = getBlockPos(); * Update the redstone input on a particular side.
for (var dir : DirectionUtil.FACINGS) updateRedstoneInput(computer, dir, pos.relative(dir)); * <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) { private void updateRedstoneInput(ServerComputer computer, Direction dir, BlockPos targetPos) {
var offsetSide = dir.getOpposite(); var offsetSide = dir.getOpposite();
var localDir = remapToLocalSide(dir); var localDir = remapToLocalSide(dir);
@ -211,6 +227,15 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
computer.setBundledRedstoneInput(localDir, BundledRedstone.getOutput(getLevel(), targetPos, offsetSide)); 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) { private void refreshPeripheral(ServerComputer computer, Direction dir) {
invalidSides &= ~(1 << dir.ordinal()); 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(); var computer = getServerComputer();
if (computer == null) return; 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 // If the position is not any adjacent one, update all inputs. This is pretty terrible, but some redstone mods
// handle this incorrectly. // handle this incorrectly.
updateRedstoneInputs(computer); for (var dir : DirectionUtil.FACINGS) updateRedstoneInput(computer, dir, getBlockPos().relative(dir));
invalidSides = (1 << 6) - 1; // Mark all peripherals as dirty. 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() { public void neighbourShapeChanged(Direction direction) {
BlockEntityHelpers.updateBlock(this); invalidSides |= 1 << direction.ordinal();
for (var dir : DirectionUtil.FACINGS) RedstoneUtil.propagateRedstoneOutput(getLevel(), getBlockPos(), dir);
var computer = getServerComputer();
if (computer != null) updateRedstoneInputs(computer);
} }
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 @Override
public final int getComputerID() { public final int getComputerID() {
@ -331,6 +384,8 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
return computer; return computer;
} }
protected abstract ServerComputer createComputer(int id);
@Nullable @Nullable
public ServerComputer getServerComputer() { public ServerComputer getServerComputer() {
return getLevel().isClientSide || getLevel().getServer() == null ? null : ServerContext.get(getLevel().getServer()).registry().get(instanceID); 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) { protected void transferStateFrom(AbstractComputerBlockEntity copy) {
if (copy.computerID != computerID || copy.instanceID != instanceID) { if (copy.computerID != computerID || !Objects.equals(copy.instanceID, instanceID)) {
unload(); unload();
instanceID = copy.instanceID; instanceID = copy.instanceID;
computerID = copy.computerID; computerID = copy.computerID;
@ -368,7 +423,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
lockCode = copy.lockCode; lockCode = copy.lockCode;
BlockEntityHelpers.updateBlock(this); BlockEntityHelpers.updateBlock(this);
} }
copy.instanceID = -1; copy.instanceID = null;
} }
@Override @Override

View File

@ -51,7 +51,7 @@ public class ComputerBlockEntity extends AbstractComputerBlockEntity {
protected void updateBlockState(ComputerState newState) { protected void updateBlockState(ComputerState newState) {
var existing = getBlockState(); var existing = getBlockState();
if (existing.getValue(ComputerBlock.STATE) != newState) { 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);
} }
} }

View File

@ -26,11 +26,13 @@ import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.AbstractContainerMenu;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function; import java.util.function.Function;
public class ServerComputer implements InputHandler, ComputerEnvironment { public class ServerComputer implements InputHandler, ComputerEnvironment {
private final int instanceID; private final int instanceID;
private final UUID instanceUUID = UUID.randomUUID();
private ServerLevel level; private ServerLevel level;
private BlockPos position; private BlockPos position;
@ -42,7 +44,6 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
private final NetworkedTerminal terminal; private final NetworkedTerminal terminal;
private final AtomicBoolean terminalChanged = new AtomicBoolean(false); private final AtomicBoolean terminalChanged = new AtomicBoolean(false);
private boolean changedLastFrame;
private int ticksSincePing; private int ticksSincePing;
public ServerComputer( public ServerComputer(
@ -96,10 +97,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
public void tickServer() { public void tickServer() {
ticksSincePing++; ticksSincePing++;
computer.tick(); computer.tick();
changedLastFrame = computer.pollAndResetChanged();
if (terminalChanged.getAndSet(false)) onTerminalChanged(); if (terminalChanged.getAndSet(false)) onTerminalChanged();
} }
@ -119,13 +117,13 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
return ticksSincePing > 100; return ticksSincePing > 100;
} }
public boolean hasOutputChanged() { public int pollAndResetChanges() {
return changedLastFrame; return computer.pollAndResetChanges();
} }
public int register() { public UUID register() {
ServerContext.get(level.getServer()).registry().add(instanceID, this); ServerContext.get(level.getServer()).registry().add(this);
return instanceID; return instanceUUID;
} }
void unload() { void unload() {
@ -134,7 +132,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
public void close() { public void close() {
unload(); unload();
ServerContext.get(level.getServer()).registry().remove(instanceID); ServerContext.get(level.getServer()).registry().remove(this);
} }
private void sendToAllInteracting(Function<AbstractContainerMenu, NetworkMessage<ClientNetworkContext>> createPacket) { private void sendToAllInteracting(Function<AbstractContainerMenu, NetworkMessage<ClientNetworkContext>> createPacket) {
@ -154,6 +152,10 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
return instanceID; return instanceID;
} }
public UUID getInstanceUUID() {
return instanceUUID;
}
public int getID() { public int getID() {
return computer.getID(); return computer.getID();
} }
@ -167,7 +169,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
} }
public ComputerState getState() { public ComputerState getState() {
if (!isOn()) return ComputerState.OFF; if (!computer.isOn()) return ComputerState.OFF;
return computer.isBlinking() ? ComputerState.BLINKING : ComputerState.ON; return computer.isBlinking() ? ComputerState.BLINKING : ComputerState.ON;
} }

View File

@ -8,14 +8,14 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Collection; import java.util.*;
import java.util.Random;
public class ServerComputerRegistry { public class ServerComputerRegistry {
private static final Random RANDOM = new Random(); private static final Random RANDOM = new Random();
private final int sessionId = RANDOM.nextInt(); 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; private int nextInstanceId;
public int getSessionID() { public int getSessionID() {
@ -28,11 +28,16 @@ public class ServerComputerRegistry {
@Nullable @Nullable
public ServerComputer get(int instanceID) { public ServerComputer get(int instanceID) {
return instanceID >= 0 ? computers.get(instanceID) : null; return instanceID >= 0 ? computersByInstanceId.get(instanceID) : null;
} }
@Nullable @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; return sessionId == this.sessionId ? get(instanceId) : null;
} }
@ -50,28 +55,36 @@ public class ServerComputerRegistry {
} }
} }
void add(int instanceID, ServerComputer computer) { void add(ServerComputer computer) {
remove(instanceID); var instanceID = computer.getInstanceID();
computers.put(instanceID, computer); var instanceUUID = computer.getInstanceUUID();
nextInstanceId = Math.max(nextInstanceId, instanceID + 1);
}
void remove(int instanceID) { if (computersByInstanceId.containsKey(instanceID)) {
var computer = get(instanceID); throw new IllegalStateException("Duplicate computer " + instanceID);
if (computer != null) {
computer.unload();
computer.onRemoved();
} }
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() { void close() {
for (var computer : getComputers()) computer.unload(); for (var computer : getComputers()) computer.unload();
computers.clear(); computersByInstanceId.clear();
computersByInstanceUuid.clear();
} }
public Collection<ServerComputer> getComputers() { public Collection<ServerComputer> getComputers() {
return computers.values(); return computersByInstanceId.values();
} }
} }

View File

@ -25,7 +25,7 @@ public class ViewComputerMenu extends ComputerMenuWithoutInventory {
private static boolean canInteractWith(ServerComputer computer, Player player) { private static boolean canInteractWith(ServerComputer computer, Player player) {
// If this computer no longer exists then discard it. // 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; return false;
} }

View File

@ -8,6 +8,7 @@ import dan200.computercraft.shared.command.text.TableBuilder;
import dan200.computercraft.shared.computer.core.ComputerState; import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.terminal.TerminalState; import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.computer.upload.UploadResult; import dan200.computercraft.shared.computer.upload.UploadResult;
import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition; import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
@ -15,7 +16,6 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundEvent;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.UUID; import java.util.UUID;
/** /**
@ -30,11 +30,11 @@ public interface ClientNetworkContext {
void handlePlayRecord(BlockPos pos, @Nullable SoundEvent sound, @Nullable String name); 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); void handleSpeakerMove(UUID source, SpeakerPosition.Message position);

View File

@ -13,24 +13,26 @@ import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.pocket.core.PocketServerComputer; import dan200.computercraft.shared.pocket.core.PocketServerComputer;
import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.FriendlyByteBuf;
import java.util.UUID;
/** /**
* Provides additional data about a client computer, such as its ID and current state. * Provides additional data about a client computer, such as its ID and current state.
*/ */
public class PocketComputerDataMessage implements NetworkMessage<ClientNetworkContext> { public class PocketComputerDataMessage implements NetworkMessage<ClientNetworkContext> {
private final int instanceId; private final UUID clientId;
private final ComputerState state; private final ComputerState state;
private final int lightState; private final int lightState;
private final TerminalState terminal; private final TerminalState terminal;
public PocketComputerDataMessage(PocketServerComputer computer, boolean sendTerminal) { public PocketComputerDataMessage(PocketServerComputer computer, boolean sendTerminal) {
instanceId = computer.getInstanceID(); clientId = computer.getInstanceUUID();
state = computer.getState(); state = computer.getState();
lightState = computer.getLight(); lightState = computer.getLight();
terminal = sendTerminal ? computer.getTerminalState() : new TerminalState((NetworkedTerminal) null); terminal = sendTerminal ? computer.getTerminalState() : new TerminalState((NetworkedTerminal) null);
} }
public PocketComputerDataMessage(FriendlyByteBuf buf) { public PocketComputerDataMessage(FriendlyByteBuf buf) {
instanceId = buf.readVarInt(); clientId = buf.readUUID();
state = buf.readEnum(ComputerState.class); state = buf.readEnum(ComputerState.class);
lightState = buf.readVarInt(); lightState = buf.readVarInt();
terminal = new TerminalState(buf); terminal = new TerminalState(buf);
@ -38,7 +40,7 @@ public class PocketComputerDataMessage implements NetworkMessage<ClientNetworkCo
@Override @Override
public void write(FriendlyByteBuf buf) { public void write(FriendlyByteBuf buf) {
buf.writeVarInt(instanceId); buf.writeUUID(clientId);
buf.writeEnum(state); buf.writeEnum(state);
buf.writeVarInt(lightState); buf.writeVarInt(lightState);
terminal.write(buf); terminal.write(buf);
@ -46,7 +48,7 @@ public class PocketComputerDataMessage implements NetworkMessage<ClientNetworkCo
@Override @Override
public void handle(ClientNetworkContext context) { public void handle(ClientNetworkContext context) {
context.handlePocketComputerData(instanceId, state, lightState, terminal); context.handlePocketComputerData(clientId, state, lightState, terminal);
} }
@Override @Override

View File

@ -9,21 +9,23 @@ import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages; import dan200.computercraft.shared.network.NetworkMessages;
import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.FriendlyByteBuf;
import java.util.UUID;
public class PocketComputerDeletedClientMessage implements NetworkMessage<ClientNetworkContext> { 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; this.instanceId = instanceId;
} }
public PocketComputerDeletedClientMessage(FriendlyByteBuf buffer) { public PocketComputerDeletedClientMessage(FriendlyByteBuf buffer) {
instanceId = buffer.readVarInt(); instanceId = buffer.readUUID();
} }
@Override @Override
public void write(FriendlyByteBuf buf) { public void write(FriendlyByteBuf buf) {
buf.writeVarInt(instanceId); buf.writeUUID(instanceId);
} }
@Override @Override

View File

@ -7,11 +7,11 @@ package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.network.MessageType; import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage; import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages; 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.SpeakerBlockEntity;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition; import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.FriendlyByteBuf;
import java.nio.ByteBuffer;
import java.util.UUID; import java.util.UUID;
/** /**
@ -24,10 +24,10 @@ import java.util.UUID;
public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkContext> { public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkContext> {
private final UUID source; private final UUID source;
private final SpeakerPosition.Message pos; private final SpeakerPosition.Message pos;
private final ByteBuffer content; private final EncodedAudio content;
private final float volume; 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.source = source;
this.pos = pos.asMessage(); this.pos = pos.asMessage();
this.content = content; this.content = content;
@ -38,10 +38,7 @@ public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkCo
source = buf.readUUID(); source = buf.readUUID();
pos = SpeakerPosition.Message.read(buf); pos = SpeakerPosition.Message.read(buf);
volume = buf.readFloat(); volume = buf.readFloat();
content = EncodedAudio.read(buf);
var bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
content = ByteBuffer.wrap(bytes);
} }
@Override @Override
@ -49,7 +46,7 @@ public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkCo
buf.writeUUID(source); buf.writeUUID(source);
pos.write(buf); pos.write(buf);
buf.writeFloat(volume); buf.writeFloat(volume);
buf.writeBytes(content.duplicate()); content.write(buf);
} }
@Override @Override

View File

@ -11,9 +11,8 @@ import dan200.computercraft.api.lua.MethodResult;
import dan200.computercraft.api.peripheral.IComputerAccess; import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IDynamicPeripheral; import dan200.computercraft.api.peripheral.IDynamicPeripheral;
import dan200.computercraft.api.peripheral.IPeripheral; 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.Direction;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntity;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -28,13 +27,16 @@ public final class GenericPeripheral implements IDynamicPeripheral {
private final Set<String> additionalTypes; private final Set<String> additionalTypes;
private final List<SaturatedMethod> methods; 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; this.side = side;
var type = RegistryHelper.getKeyOrThrow(BuiltInRegistries.BLOCK_ENTITY_TYPE, tile.getType());
this.tile = tile; this.tile = tile;
this.type = name != null ? name : type.toString(); this.type = type;
this.additionalTypes = additionalTypes; this.additionalTypes = additionalTypes;
this.methods = methods; this.methods = methods;
this.guard = () -> !tile.isRemoved() && tile.getLevel() != null && tile.getLevel().isLoaded(tile.getBlockPos());
} }
public Direction side() { public Direction side() {
@ -50,7 +52,12 @@ public final class GenericPeripheral implements IDynamicPeripheral {
@Override @Override
public MethodResult callMethod(IComputerAccess computer, ILuaContext context, int method, IArguments arguments) throws LuaException { 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 @Override

View File

@ -10,6 +10,9 @@ import dan200.computercraft.core.methods.NamedMethod;
import dan200.computercraft.core.methods.PeripheralMethod; import dan200.computercraft.core.methods.PeripheralMethod;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.world.level.block.entity.BlockEntity; 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 javax.annotation.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
@ -25,6 +28,8 @@ import java.util.Set;
* See the platform-specific peripheral providers for the usage of this. * See the platform-specific peripheral providers for the usage of this.
*/ */
final class GenericPeripheralBuilder { final class GenericPeripheralBuilder {
private static final Logger LOG = LoggerFactory.getLogger(GenericPeripheralBuilder.class);
private @Nullable String name; private @Nullable String name;
private final Set<String> additionalTypes = new HashSet<>(0); private final Set<String> additionalTypes = new HashSet<>(0);
private final ArrayList<SaturatedMethod> methods = new ArrayList<>(); private final ArrayList<SaturatedMethod> methods = new ArrayList<>();
@ -33,8 +38,24 @@ final class GenericPeripheralBuilder {
IPeripheral toPeripheral(BlockEntity blockEntity, Direction side) { IPeripheral toPeripheral(BlockEntity blockEntity, Direction side) {
if (methods.isEmpty()) return null; 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(); 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) { void addMethod(Object target, String name, PeripheralMethod method, @Nullable NamedMethod<PeripheralMethod> info) {

View File

@ -127,9 +127,8 @@ public class CableBlock extends Block implements SimpleWaterloggedBlock, EntityB
item = new ItemStack(ModRegistry.Items.CABLE.get()); 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(); cable.connectionsChanged();
if (!world.isClientSide && !player.getAbilities().instabuild) { if (!world.isClientSide && !player.getAbilities().instabuild) {
Block.popResource(world, pos, item); Block.popResource(world, pos, item);
@ -162,10 +161,7 @@ public class CableBlock extends Block implements SimpleWaterloggedBlock, EntityB
@Override @Override
public void setPlacedBy(Level world, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { public void setPlacedBy(Level world, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) {
var tile = world.getBlockEntity(pos); var tile = world.getBlockEntity(pos);
if (tile instanceof CableBlockEntity cable) { if (tile instanceof CableBlockEntity cable && cable.hasCable()) cable.connectionsChanged();
if (cable.hasCable()) cable.connectionsChanged();
}
super.setPlacedBy(world, pos, state, placer, stack); super.setPlacedBy(world, pos, state, placer, stack);
} }
@ -177,14 +173,37 @@ public class CableBlock extends Block implements SimpleWaterloggedBlock, EntityB
@Override @Override
@Deprecated @Deprecated
public BlockState updateShape(BlockState state, Direction side, BlockState otherState, LevelAccessor world, BlockPos pos, BlockPos otherPos) { public BlockState updateShape(BlockState state, Direction side, BlockState otherState, LevelAccessor level, BlockPos pos, BlockPos otherPos) {
WaterloggableHelpers.updateShape(state, world, pos); WaterloggableHelpers.updateShape(state, level, pos);
// Should never happen, but handle the case where we've no modem or cable. // Should never happen, but handle the case where we've no modem or cable.
if (!state.getValue(CABLE) && state.getValue(MODEM) == CableModemVariant.None) { if (!state.getValue(CABLE) && state.getValue(MODEM) == CableModemVariant.None) {
return getFluidState(state).createLegacyBlock(); 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 @Override
@ -230,6 +249,7 @@ public class CableBlock extends Block implements SimpleWaterloggedBlock, EntityB
@Override @Override
@Deprecated @Deprecated
public final InteractionResult use(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { 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; return world.getBlockEntity(pos) instanceof CableBlockEntity modem ? modem.use(player) : InteractionResult.PASS;
} }

View File

@ -7,7 +7,6 @@ package dan200.computercraft.shared.peripheral.modem.wired;
import dan200.computercraft.api.network.wired.WiredElement; import dan200.computercraft.api.network.wired.WiredElement;
import dan200.computercraft.api.network.wired.WiredNode; import dan200.computercraft.api.network.wired.WiredNode;
import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.command.text.ChatHelpers; import dan200.computercraft.shared.command.text.ChatHelpers;
import dan200.computercraft.shared.peripheral.modem.ModemState; import dan200.computercraft.shared.peripheral.modem.ModemState;
import dan200.computercraft.shared.platform.ComponentAccess; import dan200.computercraft.shared.platform.ComponentAccess;
@ -20,21 +19,16 @@ import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionResult; import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level; 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.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
public class CableBlockEntity extends BlockEntity { public class CableBlockEntity extends BlockEntity {
private static final String NBT_PERIPHERAL_ENABLED = "PeripheralAccess";
private final class CableElement extends WiredModemElement { private final class CableElement extends WiredModemElement {
@Override @Override
public Level getLevel() { public Level getLevel() {
@ -57,33 +51,21 @@ public class CableBlockEntity extends BlockEntity {
} }
} }
private boolean invalidPeripheral; private boolean refreshPeripheral;
private boolean peripheralAccessAllowed;
private final WiredModemLocalPeripheral peripheral = new WiredModemLocalPeripheral(PlatformHelper.get().createPeripheralAccess(this, x -> queueRefreshPeripheral())); private final WiredModemLocalPeripheral peripheral = new WiredModemLocalPeripheral(PlatformHelper.get().createPeripheralAccess(this, x -> queueRefreshPeripheral()));
private boolean connectionsFormed = false; private boolean refreshConnections = false;
private boolean connectionsChanged = false;
private final WiredModemElement cable = new CableElement(); private final WiredModemElement cable = new CableElement();
private final WiredNode node = cable.getNode(); private final WiredNode node = cable.getNode();
private final TickScheduler.Token tickToken = new TickScheduler.Token(this); private final TickScheduler.Token tickToken = new TickScheduler.Token(this);
private final WiredModemPeripheral modem = new WiredModemPeripheral( private final WiredModemPeripheral modem = new WiredModemPeripheral(
new ModemState(() -> TickScheduler.schedule(tickToken)), new ModemState(() -> TickScheduler.schedule(tickToken)), cable, peripheral, this
cable
) { ) {
@Override
protected WiredModemLocalPeripheral getLocalPeripheral() {
return peripheral;
}
@Override @Override
public Vec3 getPosition() { public Vec3 getPosition() {
return Vec3.atCenterOf(getBlockPos().relative(getDirection())); var dir = getModemDirection();
} return Vec3.atCenterOf(dir == null ? getBlockPos() : getBlockPos().relative(dir));
@Override
public Object getTarget() {
return CableBlockEntity.this;
} }
}; };
@ -93,93 +75,61 @@ public class CableBlockEntity extends BlockEntity {
super(type, pos, state); super(type, pos, state);
} }
private void onRemove() {
if (level == null || !level.isClientSide) {
node.remove();
connectionsFormed = false;
}
}
@Override @Override
public void setRemoved() { public void setRemoved() {
super.setRemoved(); super.setRemoved();
modem.removed(); modem.removed();
onRemove(); if (level == null || !level.isClientSide) node.remove();
} }
@Override @Override
public void clearRemoved() { public void clearRemoved() {
super.clearRemoved(); super.clearRemoved();
refreshConnections = refreshPeripheral = true;
TickScheduler.schedule(tickToken); TickScheduler.schedule(tickToken);
} }
@Override @Override
@Deprecated @Deprecated
public void setBlockState(BlockState state) { public void setBlockState(BlockState state) {
var direction = getMaybeDirection(); var direction = getModemDirection();
var hasCable = hasCable();
super.setBlockState(state); super.setBlockState(state);
// We invalidate both the modem and element if the modem's direction is different. // We invalidate both the modem and element if the modem direction or cable are different.
if (getMaybeDirection() != direction) PlatformHelper.get().invalidateComponent(this); if (hasCable() != hasCable || getModemDirection() != direction) PlatformHelper.get().invalidateComponent(this);
} }
@Nullable @Nullable
private Direction getMaybeDirection() { private Direction getModemDirection() {
return getBlockState().getValue(CableBlock.MODEM).getFacing(); return getBlockState().getValue(CableBlock.MODEM).getFacing();
} }
private Direction getDirection() {
var direction = getMaybeDirection();
return direction == null ? Direction.NORTH : direction;
}
void neighborChanged(BlockPos neighbour) { void neighborChanged(BlockPos neighbour) {
var dir = getDirection(); var dir = getModemDirection();
if (neighbour.equals(getBlockPos().relative(dir)) && hasModem() && !getBlockState().canSurvive(getLevel(), getBlockPos())) { if (!level.isClientSide && dir != null && getBlockPos().relative(dir).equals(neighbour) && isPeripheralOn()) {
if (hasCable()) { queueRefreshPeripheral();
// 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();
} }
} }
private void queueRefreshPeripheral() { void queueRefreshPeripheral() {
if (invalidPeripheral) return; refreshPeripheral = true;
invalidPeripheral = true;
TickScheduler.schedule(tickToken); TickScheduler.schedule(tickToken);
} }
private void refreshPeripheral() {
invalidPeripheral = false;
if (level != null && !isRemoved() && peripheral.attach(level, getBlockPos(), getDirection())) {
updateConnectedPeripherals();
}
}
InteractionResult use(Player player) { InteractionResult use(Player player) {
if (player.isCrouching() || !player.mayBuild()) return InteractionResult.PASS;
if (!canAttachPeripheral()) return InteractionResult.FAIL; if (!canAttachPeripheral()) return InteractionResult.FAIL;
if (getLevel().isClientSide) return InteractionResult.SUCCESS; if (getLevel().isClientSide) return InteractionResult.SUCCESS;
var oldName = peripheral.getConnectedName(); var oldName = peripheral.getConnectedName();
togglePeripheralAccess(); if (isPeripheralOn()) {
detachPeripheral();
} else {
attachPeripheral();
}
var newName = peripheral.getConnectedName(); var newName = peripheral.getConnectedName();
if (!Objects.equals(newName, oldName)) { if (!Objects.equals(newName, oldName)) {
if (oldName != null) { if (oldName != null) {
player.displayClientMessage(Component.translatable("chat.computercraft.wired_modem.peripheral_disconnected", player.displayClientMessage(Component.translatable("chat.computercraft.wired_modem.peripheral_disconnected",
@ -197,14 +147,11 @@ public class CableBlockEntity extends BlockEntity {
@Override @Override
public void load(CompoundTag nbt) { public void load(CompoundTag nbt) {
super.load(nbt); super.load(nbt);
// Fallback to the previous (incorrect) key
peripheralAccessAllowed = nbt.getBoolean(NBT_PERIPHERAL_ENABLED) || nbt.getBoolean("PeirpheralAccess");
peripheral.read(nbt, ""); peripheral.read(nbt, "");
} }
@Override @Override
public void saveAdditional(CompoundTag nbt) { public void saveAdditional(CompoundTag nbt) {
nbt.putBoolean(NBT_PERIPHERAL_ENABLED, peripheralAccessAllowed);
peripheral.write(nbt, ""); peripheral.write(nbt, "");
super.saveAdditional(nbt); super.saveAdditional(nbt);
} }
@ -213,7 +160,7 @@ public class CableBlockEntity extends BlockEntity {
var state = getBlockState(); var state = getBlockState();
var oldVariant = state.getValue(CableBlock.MODEM); var oldVariant = state.getValue(CableBlock.MODEM);
var newVariant = CableModemVariant var newVariant = CableModemVariant
.from(oldVariant.getFacing(), modem.getModemState().isOpen(), peripheralAccessAllowed); .from(oldVariant.getFacing(), modem.getModemState().isOpen(), peripheral.hasPeripheral());
if (oldVariant != newVariant) { if (oldVariant != newVariant) {
level.setBlockAndUpdate(getBlockPos(), state.setValue(CableBlock.MODEM, newVariant)); level.setBlockAndUpdate(getBlockPos(), state.setValue(CableBlock.MODEM, newVariant));
@ -223,31 +170,24 @@ public class CableBlockEntity extends BlockEntity {
void blockTick() { void blockTick() {
if (getLevel().isClientSide) return; if (getLevel().isClientSide) return;
if (invalidPeripheral) refreshPeripheral(); if (refreshPeripheral) {
refreshPeripheral = false;
if (isPeripheralOn()) attachPeripheral();
}
if (modem.getModemState().pollChanged()) updateBlockState(); if (modem.getModemState().pollChanged()) updateBlockState();
if (!connectionsFormed) { if (refreshConnections) connectionsChanged();
connectionsFormed = true;
connectionsChanged();
if (peripheralAccessAllowed) {
peripheral.attach(level, worldPosition, getDirection());
updateConnectedPeripherals();
}
}
if (connectionsChanged) connectionsChanged();
} }
private void scheduleConnectionsChanged() { void scheduleConnectionsChanged() {
connectionsChanged = true; refreshConnections = true;
TickScheduler.schedule(tickToken); TickScheduler.schedule(tickToken);
} }
void connectionsChanged() { void connectionsChanged() {
if (getLevel().isClientSide) return; if (getLevel().isClientSide) return;
connectionsChanged = false; refreshConnections = false;
var state = getBlockState(); var state = getBlockState();
var world = getLevel(); var world = getLevel();
@ -263,56 +203,29 @@ public class CableBlockEntity extends BlockEntity {
if (CableBlock.canConnectIn(state, facing)) { if (CableBlock.canConnectIn(state, facing)) {
// If we can connect to it then do so // If we can connect to it then do so
this.node.connectTo(node); this.node.connectTo(node);
} else if (this.node.getNetwork() == node.getNetwork()) { } else {
// Otherwise if we're on the same network then attempt to void it. // Otherwise break the connection.
this.node.disconnectFrom(node); this.node.disconnectFrom(node);
} }
} }
// If we can no longer attach peripherals, then detach any which may have existed
if (!canAttachPeripheral()) detachPeripheral();
} }
void modemChanged() { private void attachPeripheral() {
// Tell anyone who cares that the connection state has changed var dir = Objects.requireNonNull(getModemDirection(), "Attaching without a modem");
PlatformHelper.get().invalidateComponent(this); if (peripheral.attach(getLevel(), getBlockPos(), dir)) updateConnectedPeripherals();
updateBlockState();
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 togglePeripheralAccess() { private void detachPeripheral() {
if (!peripheralAccessAllowed) { if (peripheral.detach()) updateConnectedPeripherals();
peripheral.attach(level, getBlockPos(), getDirection());
if (!peripheral.hasPeripheral()) return;
peripheralAccessAllowed = true;
node.updatePeripherals(peripheral.toMap());
} else {
peripheral.detach();
peripheralAccessAllowed = false;
node.updatePeripherals(Map.of());
}
updateBlockState(); updateBlockState();
} }
private void updateConnectedPeripherals() { private void updateConnectedPeripherals() {
var peripherals = peripheral.toMap(); node.updatePeripherals(peripheral.toMap());
if (peripherals.isEmpty()) {
// If there are no peripherals then disable access and update the display state.
peripheralAccessAllowed = false;
updateBlockState();
}
node.updatePeripherals(peripherals);
} }
@Nullable @Nullable
@ -322,7 +235,11 @@ public class CableBlockEntity extends BlockEntity {
@Nullable @Nullable
public IPeripheral getPeripheral(@Nullable Direction direction) { 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() { boolean hasCable() {

View File

@ -31,15 +31,12 @@ public abstract class CableBlockItem extends BlockItem {
// TODO: Check entity collision. // TODO: Check entity collision.
if (!state.canSurvive(world, pos)) return false; if (!state.canSurvive(world, pos)) return false;
world.setBlock(pos, state, 3); world.setBlockAndUpdate(pos, state);
var soundType = state.getBlock().getSoundType(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); world.playSound(null, pos, soundType.getPlaceSound(), SoundSource.BLOCKS, (soundType.getVolume() + 1.0F) / 2.0F, soundType.getPitch() * 0.8F);
var tile = world.getBlockEntity(pos); var tile = world.getBlockEntity(pos);
if (tile instanceof CableBlockEntity cable) { if (tile instanceof CableBlockEntity cable) cable.connectionsChanged();
cable.modemChanged();
cable.connectionsChanged();
}
return true; return true;
} }

View File

@ -10,49 +10,57 @@ import net.minecraft.util.StringRepresentable;
import javax.annotation.Nullable; import javax.annotation.Nullable;
public enum CableModemVariant implements StringRepresentable { public enum CableModemVariant implements StringRepresentable {
None("none", null), None("none", null, false, false),
DownOff("down_off", Direction.DOWN), DownOff("down_off", Direction.DOWN, false, false),
UpOff("up_off", Direction.UP), UpOff("up_off", Direction.UP, false, false),
NorthOff("north_off", Direction.NORTH), NorthOff("north_off", Direction.NORTH, false, false),
SouthOff("south_off", Direction.SOUTH), SouthOff("south_off", Direction.SOUTH, false, false),
WestOff("west_off", Direction.WEST), WestOff("west_off", Direction.WEST, false, false),
EastOff("east_off", Direction.EAST), EastOff("east_off", Direction.EAST, false, false),
DownOn("down_on", Direction.DOWN), DownOn("down_on", Direction.DOWN, true, false),
UpOn("up_on", Direction.UP), UpOn("up_on", Direction.UP, true, false),
NorthOn("north_on", Direction.NORTH), NorthOn("north_on", Direction.NORTH, true, false),
SouthOn("south_on", Direction.SOUTH), SouthOn("south_on", Direction.SOUTH, true, false),
WestOn("west_on", Direction.WEST), WestOn("west_on", Direction.WEST, true, false),
EastOn("east_on", Direction.EAST), EastOn("east_on", Direction.EAST, true, false),
DownOffPeripheral("down_off_peripheral", Direction.DOWN), DownOffPeripheral("down_off_peripheral", Direction.DOWN, false, true),
UpOffPeripheral("up_off_peripheral", Direction.UP), UpOffPeripheral("up_off_peripheral", Direction.UP, false, true),
NorthOffPeripheral("north_off_peripheral", Direction.NORTH), NorthOffPeripheral("north_off_peripheral", Direction.NORTH, false, true),
SouthOffPeripheral("south_off_peripheral", Direction.SOUTH), SouthOffPeripheral("south_off_peripheral", Direction.SOUTH, false, true),
WestOffPeripheral("west_off_peripheral", Direction.WEST), WestOffPeripheral("west_off_peripheral", Direction.WEST, false, true),
EastOffPeripheral("east_off_peripheral", Direction.EAST), EastOffPeripheral("east_off_peripheral", Direction.EAST, false, true),
DownOnPeripheral("down_on_peripheral", Direction.DOWN), DownOnPeripheral("down_on_peripheral", Direction.DOWN, true, true),
UpOnPeripheral("up_on_peripheral", Direction.UP), UpOnPeripheral("up_on_peripheral", Direction.UP, true, true),
NorthOnPeripheral("north_on_peripheral", Direction.NORTH), NorthOnPeripheral("north_on_peripheral", Direction.NORTH, true, true),
SouthOnPeripheral("south_on_peripheral", Direction.SOUTH), SouthOnPeripheral("south_on_peripheral", Direction.SOUTH, true, true),
WestOnPeripheral("west_on_peripheral", Direction.WEST), WestOnPeripheral("west_on_peripheral", Direction.WEST, true, true),
EastOnPeripheral("east_on_peripheral", Direction.EAST); EastOnPeripheral("east_on_peripheral", Direction.EAST, true, true);
private static final CableModemVariant[] VALUES = values(); private static final CableModemVariant[] VALUES = values();
private final String name; private final String name;
private final @Nullable Direction facing; 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.name = name;
this.facing = facing; 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) { 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) { public static CableModemVariant from(@Nullable Direction facing, boolean modem, boolean peripheral) {
var state = (modem ? 1 : 0) + (peripheral ? 2 : 0); return VALUES[getIndex(facing, modem, peripheral)];
return facing == null ? None : VALUES[1 + 6 * state + facing.get3DDataValue()];
} }
@Override @Override
@ -64,6 +72,14 @@ public enum CableModemVariant implements StringRepresentable {
return facing; return facing;
} }
public boolean isModemOn() {
return modemOn;
}
public boolean isPeripheralOn() {
return peripheralOn;
}
@Override @Override
public String toString() { public String toString() {
return name; return name;

View File

@ -7,12 +7,14 @@ package dan200.computercraft.shared.peripheral.modem.wired;
import dan200.computercraft.annotations.ForgeOverride; import dan200.computercraft.annotations.ForgeOverride;
import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.ModRegistry;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.RandomSource; import net.minecraft.util.RandomSource;
import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult; import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.LevelReader; import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.EntityBlock;
@ -49,13 +51,27 @@ public class WiredModemFullBlock extends Block implements EntityBlock {
@Override @Override
@Deprecated @Deprecated
public final void neighborChanged(BlockState state, Level world, BlockPos pos, Block neighbourBlock, BlockPos neighbourPos, boolean isMoving) { public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor level, BlockPos pos, BlockPos neighborPos) {
if (world.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) modem.neighborChanged(neighbourPos); 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 @ForgeOverride
public final void onNeighborChange(BlockState state, LevelReader world, BlockPos pos, BlockPos neighbour) { public final void onNeighborChange(BlockState state, LevelReader level, BlockPos pos, BlockPos neighbour) {
if (world.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) modem.neighborChanged(neighbour); if (state.getValue(PERIPHERAL_ON) && level.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) {
modem.neighborChanged(neighbour);
}
} }
@Override @Override

View File

@ -32,8 +32,6 @@ import static dan200.computercraft.shared.peripheral.modem.wired.WiredModemFullB
import static dan200.computercraft.shared.peripheral.modem.wired.WiredModemFullBlock.PERIPHERAL_ON; import static dan200.computercraft.shared.peripheral.modem.wired.WiredModemFullBlock.PERIPHERAL_ON;
public class WiredModemFullBlockEntity extends BlockEntity { public class WiredModemFullBlockEntity extends BlockEntity {
private static final String NBT_PERIPHERAL_ENABLED = "PeripheralAccess";
private static final class FullElement extends WiredModemElement { private static final class FullElement extends WiredModemElement {
private final WiredModemFullBlockEntity entity; private final WiredModemFullBlockEntity entity;
@ -70,11 +68,9 @@ public class WiredModemFullBlockEntity extends BlockEntity {
private final WiredModemPeripheral[] modems = new WiredModemPeripheral[6]; private final WiredModemPeripheral[] modems = new WiredModemPeripheral[6];
private boolean peripheralAccessAllowed = false;
private final WiredModemLocalPeripheral[] peripherals = new WiredModemLocalPeripheral[6]; private final WiredModemLocalPeripheral[] peripherals = new WiredModemLocalPeripheral[6];
private boolean connectionsFormed = false; private boolean refreshConnections = false;
private boolean connectionsChanged = false;
private final TickScheduler.Token tickToken = new TickScheduler.Token(this); private final TickScheduler.Token tickToken = new TickScheduler.Token(this);
private final ModemState modemState = new ModemState(() -> TickScheduler.schedule(tickToken)); private final ModemState modemState = new ModemState(() -> TickScheduler.schedule(tickToken));
@ -96,31 +92,30 @@ public class WiredModemFullBlockEntity extends BlockEntity {
@Override @Override
public void setRemoved() { public void setRemoved() {
super.setRemoved(); super.setRemoved();
if (level == null || !level.isClientSide) {
node.remove(); for (var modem : modems) {
connectionsFormed = false; 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) { void neighborChanged(BlockPos neighbour) {
if (!level.isClientSide && peripheralAccessAllowed) { for (var facing : DirectionUtil.FACINGS) {
for (var facing : DirectionUtil.FACINGS) { if (getBlockPos().relative(facing).equals(neighbour)) queueRefreshPeripheral(facing);
if (getBlockPos().relative(facing).equals(neighbour)) queueRefreshPeripheral(facing);
}
} }
} }
private void queueRefreshPeripheral(Direction facing) { void queueRefreshPeripheral(Direction facing) {
if (invalidSides == 0) TickScheduler.schedule(tickToken);
invalidSides |= 1 << facing.ordinal(); invalidSides |= 1 << facing.ordinal();
} TickScheduler.schedule(tickToken);
private void refreshPeripheral(Direction facing) {
invalidSides &= ~(1 << facing.ordinal());
var peripheral = peripherals[facing.ordinal()];
if (level != null && !isRemoved() && peripheral.attach(level, getBlockPos(), facing)) {
updateConnectedPeripherals();
}
} }
public InteractionResult use(Player player) { public InteractionResult use(Player player) {
@ -129,7 +124,11 @@ public class WiredModemFullBlockEntity extends BlockEntity {
// On server, we interacted if a peripheral was found // On server, we interacted if a peripheral was found
var oldPeriphNames = getConnectedPeripheralNames(); var oldPeriphNames = getConnectedPeripheralNames();
togglePeripheralAccess(); if (isPeripheralOn()) {
detachPeripherals();
} else {
attachPeripherals(DirectionUtil.ALL_SIDES);
}
var periphNames = getConnectedPeripheralNames(); var periphNames = getConnectedPeripheralNames();
if (!Objects.equals(periphNames, oldPeriphNames)) { if (!Objects.equals(periphNames, oldPeriphNames)) {
@ -158,65 +157,45 @@ public class WiredModemFullBlockEntity extends BlockEntity {
@Override @Override
public void load(CompoundTag nbt) { public void load(CompoundTag nbt) {
super.load(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)); for (var i = 0; i < peripherals.length; i++) peripherals[i].read(nbt, Integer.toString(i));
} }
@Override @Override
public void saveAdditional(CompoundTag nbt) { 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)); for (var i = 0; i < peripherals.length; i++) peripherals[i].write(nbt, Integer.toString(i));
super.saveAdditional(nbt); 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() { void blockTick() {
if (getLevel().isClientSide) return; if (getLevel().isClientSide) return;
if (invalidSides != 0) { if (invalidSides != 0) {
for (var direction : DirectionUtil.FACINGS) { var oldInvalidSides = invalidSides;
if ((invalidSides & (1 << direction.ordinal())) != 0) refreshPeripheral(direction); invalidSides = 0;
} if (isPeripheralOn()) attachPeripherals(oldInvalidSides);
} }
if (modemState.pollChanged()) updateBlockState(); if (modemState.pollChanged()) updateModemBlockState();
if (!connectionsFormed) { if (refreshConnections) connectionsChanged();
connectionsFormed = true; }
connectionsChanged(); private void updateModemBlockState() {
if (peripheralAccessAllowed) { var state = getBlockState();
for (var facing : DirectionUtil.FACINGS) { var modemOn = modemState.isOpen();
peripherals[facing.ordinal()].attach(level, getBlockPos(), facing); if (state.getValue(MODEM_ON) == modemOn) return;
}
updateConnectedPeripherals();
}
}
if (connectionsChanged) connectionsChanged(); getLevel().setBlockAndUpdate(getBlockPos(), state.setValue(MODEM_ON, modemOn));
} }
private void scheduleConnectionsChanged() { private void scheduleConnectionsChanged() {
connectionsChanged = true; refreshConnections = true;
TickScheduler.schedule(tickToken); TickScheduler.schedule(tickToken);
} }
private void connectionsChanged() { private void connectionsChanged() {
if (getLevel().isClientSide) return; if (getLevel().isClientSide) return;
connectionsChanged = false; refreshConnections = false;
var world = getLevel(); var world = getLevel();
var current = getBlockPos(); var current = getBlockPos();
@ -231,57 +210,48 @@ public class WiredModemFullBlockEntity extends BlockEntity {
} }
} }
private void togglePeripheralAccess() { private List<String> getConnectedPeripheralNames() {
if (!peripheralAccessAllowed) { List<String> peripherals = new ArrayList<>(6);
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);
for (var peripheral : this.peripherals) { for (var peripheral : this.peripherals) {
var name = peripheral.getConnectedName(); var name = peripheral.getConnectedName();
if (name != null) peripherals.add(name); if (name != null) peripherals.add(name);
} }
peripherals.sort(String::compareTo);
return peripherals; return peripherals;
} }
private Map<String, IPeripheral> getConnectedPeripherals() { private void attachPeripherals(int sides) {
if (!peripheralAccessAllowed) return Map.of(); var anyChanged = false;
Map<String, IPeripheral> peripherals = new HashMap<>(6); Map<String, IPeripheral> attachedPeripherals = new HashMap<>(6);
for (var peripheral : this.peripherals) peripheral.extendMap(peripherals);
return Collections.unmodifiableMap(peripherals);
}
private void updateConnectedPeripherals() { for (var facing : DirectionUtil.FACINGS) {
var peripherals = getConnectedPeripherals(); var peripheral = peripherals[facing.ordinal()];
if (peripherals.isEmpty()) { if (DirectionUtil.isSet(sides, facing)) anyChanged |= peripheral.attach(getLevel(), getBlockPos(), facing);
// If there are no peripherals then disable access and update the display state. peripheral.extendMap(attachedPeripherals);
peripheralAccessAllowed = false;
updateBlockState();
} }
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() { public WiredElement getElement() {
@ -295,22 +265,11 @@ public class WiredModemFullBlockEntity extends BlockEntity {
var peripheral = modems[side.ordinal()]; var peripheral = modems[side.ordinal()];
if (peripheral != null) return peripheral; if (peripheral != null) return peripheral;
var localPeripheral = peripherals[side.ordinal()]; return modems[side.ordinal()] = new WiredModemPeripheral(modemState, element, peripherals[side.ordinal()], this) {
return modems[side.ordinal()] = new WiredModemPeripheral(modemState, element) {
@Override
protected WiredModemLocalPeripheral getLocalPeripheral() {
return localPeripheral;
}
@Override @Override
public Vec3 getPosition() { public Vec3 getPosition() {
return Vec3.atCenterOf(getBlockPos().relative(side)); return Vec3.atCenterOf(getBlockPos().relative(side));
} }
@Override
public Object getTarget() {
return WiredModemFullBlockEntity.this;
}
}; };
} }
} }

View File

@ -6,6 +6,7 @@ package dan200.computercraft.shared.peripheral.modem.wired;
import dan200.computercraft.api.ComputerCraftTags; import dan200.computercraft.api.ComputerCraftTags;
import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.core.util.PeripheralHelpers;
import dan200.computercraft.shared.computer.core.ServerContext; import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.platform.ComponentAccess; import dan200.computercraft.shared.platform.ComponentAccess;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
@ -15,7 +16,6 @@ import net.minecraft.nbt.Tag;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Collections;
import java.util.Map; import java.util.Map;
import static dan200.computercraft.core.util.Nullability.assertNonNull; 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); 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; return peripheral != null ? type + "_" + id : null;
} }
@Nullable
public IPeripheral getPeripheral() {
return peripheral;
}
public boolean hasPeripheral() { public boolean hasPeripheral() {
return peripheral != null; return peripheral != null;
} }
@ -100,9 +95,7 @@ public final class WiredModemLocalPeripheral {
} }
public Map<String, IPeripheral> toMap() { public Map<String, IPeripheral> toMap() {
return peripheral == null return peripheral == null ? Map.of() : Map.of(type + "_" + id, peripheral);
? Map.of()
: Collections.singletonMap(type + "_" + id, peripheral);
} }
public void write(CompoundTag tag, String suffix) { public void write(CompoundTag tag, String suffix) {

View File

@ -15,6 +15,7 @@ import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.peripheral.NotAttachedException; import dan200.computercraft.api.peripheral.NotAttachedException;
import dan200.computercraft.api.peripheral.WorkMonitor; import dan200.computercraft.api.peripheral.WorkMonitor;
import dan200.computercraft.core.apis.PeripheralAPI; import dan200.computercraft.core.apis.PeripheralAPI;
import dan200.computercraft.core.computer.GuardedLuaContext;
import dan200.computercraft.core.methods.PeripheralMethod; import dan200.computercraft.core.methods.PeripheralMethod;
import dan200.computercraft.core.util.LuaUtil; import dan200.computercraft.core.util.LuaUtil;
import dan200.computercraft.shared.computer.core.ServerContext; 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 dan200.computercraft.shared.peripheral.modem.ModemState;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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 static final Logger LOG = LoggerFactory.getLogger(WiredModemPeripheral.class);
private final WiredModemElement modem; private final WiredModemElement modem;
private final WiredModemLocalPeripheral localPeripheral;
private final BlockEntity target;
private final Map<IComputerAccess, ConcurrentMap<String, RemotePeripheralWrapper>> peripheralWrappers = new HashMap<>(1); 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); super(state);
this.modem = modem; this.modem = modem;
this.localPeripheral = localPeripheral;
this.target = target;
} }
//region IPacketSender implementation //region IPacketSender implementation
@ -62,8 +73,6 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
public Level getLevel() { public Level getLevel() {
return modem.getLevel(); return modem.getLevel();
} }
protected abstract WiredModemLocalPeripheral getLocalPeripheral();
//endregion //endregion
@Override @Override
@ -207,7 +216,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
*/ */
@LuaFunction @LuaFunction
public final @Nullable Object[] getNameLocal() { public final @Nullable Object[] getNameLocal() {
var local = getLocalPeripheral().getConnectedName(); var local = localPeripheral.getConnectedName();
return local == null ? null : new Object[]{ local }; return local == null ? null : new Object[]{ local };
} }
@ -218,8 +227,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
ConcurrentMap<String, RemotePeripheralWrapper> wrappers; ConcurrentMap<String, RemotePeripheralWrapper> wrappers;
synchronized (peripheralWrappers) { synchronized (peripheralWrappers) {
wrappers = peripheralWrappers.get(computer); wrappers = peripheralWrappers.computeIfAbsent(computer, k -> new ConcurrentHashMap<>());
if (wrappers == null) peripheralWrappers.put(computer, wrappers = new ConcurrentHashMap<>());
} }
synchronized (modem.getRemotePeripherals()) { synchronized (modem.getRemotePeripherals()) {
@ -245,11 +253,13 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
} }
@Override @Override
public boolean equals(@Nullable IPeripheral other) { public final boolean equals(@Nullable IPeripheral other) {
if (other instanceof WiredModemPeripheral otherModem) { return other instanceof WiredModemPeripheral otherModem && otherModem.modem == modem;
return otherModem.modem == modem; }
}
return false; @Override
public final Object getTarget() {
return target;
} }
//endregion //endregion
@ -272,12 +282,11 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
var wrapper = wrappers.remove(name); var wrapper = wrappers.remove(name);
if (wrapper != null) wrapper.detach(); if (wrapper != null) wrapper.detach();
} }
} }
} }
private void attachPeripheralImpl(IComputerAccess computer, ConcurrentMap<String, RemotePeripheralWrapper> peripherals, String periphName, IPeripheral peripheral) { 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 methods = ServerContext.get(((ServerLevel) getLevel()).getServer()).peripheralMethods().getSelfMethods(peripheral);
var wrapper = new RemotePeripheralWrapper(modem, peripheral, computer, periphName, methods); var wrapper = new RemotePeripheralWrapper(modem, peripheral, computer, periphName, methods);
peripherals.put(periphName, wrapper); peripherals.put(periphName, wrapper);
@ -296,7 +305,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
return wrappers == null ? null : wrappers.get(remoteName); 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 WiredModemElement element;
private final IPeripheral peripheral; private final IPeripheral peripheral;
private final IComputerAccess computer; private final IComputerAccess computer;
@ -309,6 +318,8 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
private volatile boolean attached; private volatile boolean attached;
private final Set<String> mounts = new HashSet<>(); private final Set<String> mounts = new HashSet<>();
private @Nullable GuardedLuaContext contextWrapper;
RemotePeripheralWrapper(WiredModemElement element, IPeripheral peripheral, IComputerAccess computer, String name, Map<String, PeripheralMethod> methods) { RemotePeripheralWrapper(WiredModemElement element, IPeripheral peripheral, IComputerAccess computer, String name, Map<String, PeripheralMethod> methods) {
this.element = element; this.element = element;
this.peripheral = peripheral; 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 { public MethodResult callMethod(ILuaContext context, String methodName, IArguments arguments) throws LuaException {
var method = methodMap.get(methodName); var method = methodMap.get(methodName);
if (method == null) throw new LuaException("No such method " + 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 // IComputerAccess implementation

View File

@ -44,13 +44,18 @@ public class MonitorBlockEntity extends BlockEntity {
private final boolean advanced; private final boolean advanced;
private @Nullable ServerMonitor serverMonitor; 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 ClientMonitor clientMonitor;
private @Nullable MonitorPeripheral peripheral; private @Nullable MonitorPeripheral peripheral;
private final Set<IComputerAccess> computers = Collections.newSetFromMap(new ConcurrentHashMap<>()); private final Set<IComputerAccess> computers = Collections.newSetFromMap(new ConcurrentHashMap<>());
private boolean needsUpdate = false; private boolean needsUpdate = false;
private boolean needsValidating = false; private boolean needsValidating = false;
private boolean destroyed = false;
// MonitorWatcher state. // MonitorWatcher state.
boolean enqueued; boolean enqueued;
@ -89,7 +94,7 @@ public class MonitorBlockEntity extends BlockEntity {
@Override @Override
public void setRemoved() { public void setRemoved() {
super.setRemoved(); super.setRemoved();
if (clientMonitor != null && xIndex == 0 && yIndex == 0) clientMonitor.destroy(); if (clientMonitor != null) clientMonitor.destroy();
} }
@Override @Override
@ -143,7 +148,7 @@ public class MonitorBlockEntity extends BlockEntity {
private ServerMonitor getServerMonitor() { private ServerMonitor getServerMonitor() {
if (serverMonitor != null) return serverMonitor; if (serverMonitor != null) return serverMonitor;
var origin = getOrigin().getMonitor(); var origin = getOrigin();
if (origin == null) return null; if (origin == null) return null;
return serverMonitor = origin.serverMonitor; return serverMonitor = origin.serverMonitor;
@ -182,13 +187,11 @@ public class MonitorBlockEntity extends BlockEntity {
} }
@Nullable @Nullable
public ClientMonitor getClientMonitor() { public ClientMonitor getOriginClientMonitor() {
if (clientMonitor != null) return clientMonitor; if (clientMonitor != null) return clientMonitor;
var te = level.getBlockEntity(toWorldPos(0, 0)); var origin = getOrigin();
if (!(te instanceof MonitorBlockEntity monitor)) return null; return origin == null ? null : origin.clientMonitor;
return clientMonitor = monitor.clientMonitor;
} }
// Networking stuff // Networking stuff
@ -209,17 +212,14 @@ public class MonitorBlockEntity extends BlockEntity {
} }
private void onClientLoad(int oldXIndex, int oldYIndex) { private void onClientLoad(int oldXIndex, int oldYIndex) {
if (oldXIndex != xIndex || oldYIndex != yIndex) { if ((oldXIndex != xIndex || oldYIndex != yIndex) && clientMonitor != null) {
// If our index has changed then it's possible the origin monitor has changed. Thus // If our index has changed, and we were the origin, then destroy the current monitor.
// we'll clear our cache. If we're the origin then we'll need to remove the glList as well. clientMonitor.destroy();
if (oldXIndex == 0 && oldYIndex == 0 && clientMonitor != null) clientMonitor.destroy();
clientMonitor = null; clientMonitor = null;
} }
if (xIndex == 0 && yIndex == 0) { // If we're the origin terminal then create it.
// If we're the origin terminal then create it. if (xIndex == 0 && yIndex == 0 && clientMonitor == null) clientMonitor = new ClientMonitor(this);
if (clientMonitor == null) clientMonitor = new ClientMonitor(this);
}
} }
public final void read(TerminalState state) { public final void read(TerminalState state) {
@ -286,7 +286,7 @@ public class MonitorBlockEntity extends BlockEntity {
} }
boolean isCompatible(MonitorBlockEntity other) { 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; return isCompatible(monitor) ? MonitorState.present(monitor) : MonitorState.MISSING;
} }
private MonitorState getOrigin() { private @Nullable MonitorBlockEntity getOrigin() {
return getLoadedMonitor(0, 0); return getLoadedMonitor(0, 0).getMonitor();
} }
/** /**
@ -389,7 +389,7 @@ public class MonitorBlockEntity extends BlockEntity {
} }
void expand() { void expand() {
var monitor = getOrigin().getMonitor(); var monitor = getOrigin();
if (monitor != null && monitor.xIndex == 0 && monitor.yIndex == 0) new Expander(monitor).expand(); 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 hasPeripheral = false;
var origin = getOrigin().getMonitor(); var origin = getOrigin();
var serverMonitor = origin != null ? origin.serverMonitor : this.serverMonitor; var serverMonitor = origin != null ? origin.serverMonitor : this.serverMonitor;
for (var x = 0; x < width; x++) { for (var x = 0; x < width; x++) {
for (var y = 0; y < height; y++) { for (var y = 0; y < height; y++) {

View File

@ -37,7 +37,7 @@ class DfpwmState {
private boolean unplayed = true; private boolean unplayed = true;
private long clientEndTime = PauseAwareTimer.getTime(); private long clientEndTime = PauseAwareTimer.getTime();
private float pendingVolume = 1.0f; 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 { synchronized boolean pushBuffer(LuaTable<?, ?> table, int size, Optional<Double> volume) throws LuaException {
if (pendingAudio != null) return false; if (pendingAudio != null) return false;
@ -45,6 +45,10 @@ class DfpwmState {
var outSize = size / 8; var outSize = size / 8;
var buffer = ByteBuffer.allocate(outSize); var buffer = ByteBuffer.allocate(outSize);
var initialCharge = charge;
var initialStrength = strength;
var initialPreviousBit = previousBit;
for (var i = 0; i < outSize; i++) { for (var i = 0; i < outSize; i++) {
var thisByte = 0; var thisByte = 0;
for (var j = 1; j <= 8; j++) { for (var j = 1; j <= 8; j++) {
@ -80,7 +84,7 @@ class DfpwmState {
buffer.flip(); buffer.flip();
pendingAudio = buffer; pendingAudio = new EncodedAudio(initialCharge, initialStrength, initialPreviousBit, buffer);
pendingVolume = (float) clampVolume(volume.orElse((double) pendingVolume)); pendingVolume = (float) clampVolume(volume.orElse((double) pendingVolume));
return true; return true;
} }
@ -89,12 +93,12 @@ class DfpwmState {
return pendingAudio != null && now >= clientEndTime - CLIENT_BUFFER; return pendingAudio != null && now >= clientEndTime - CLIENT_BUFFER;
} }
ByteBuffer pullPending(long now) { EncodedAudio pullPending(long now) {
var audio = pendingAudio; var audio = pendingAudio;
if (audio == null) throw new IllegalStateException("Should not pull pending audio yet"); if (audio == null) throw new IllegalStateException("Should not pull pending audio yet");
pendingAudio = null; pendingAudio = null;
// Compute when we should consider sending the next packet. // 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; unplayed = false;
return audio; return audio;
} }

View File

@ -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));
}
}

View File

@ -375,4 +375,13 @@ public interface PlatformHelper extends dan200.computercraft.impl.PlatformHelper
* @see ServerPlayerGameMode#useItemOn(ServerPlayer, Level, ItemStack, InteractionHand, BlockHitResult) * @see ServerPlayerGameMode#useItemOn(ServerPlayer, Level, ItemStack, InteractionHand, BlockHitResult)
*/ */
InteractionResult useOn(ServerPlayer player, ItemStack stack, BlockHitResult hit, Predicate<BlockState> canUseBlock); 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;
}
} }

View File

@ -10,6 +10,7 @@ import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.core.computer.ComputerSide; import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.shared.common.IColouredItem; import dan200.computercraft.shared.common.IColouredItem;
import dan200.computercraft.shared.computer.core.ComputerFamily; 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.core.ServerComputer;
import dan200.computercraft.shared.config.Config; import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.network.client.PocketComputerDataMessage; import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
@ -38,7 +39,10 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
private ItemStack stack = ItemStack.EMPTY; private ItemStack stack = ItemStack.EMPTY;
private int lightColour = -1; 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<>(); private final Set<ServerPlayer> tracking = new HashSet<>();
@ -83,10 +87,7 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
@Override @Override
public void setLight(int colour) { public void setLight(int colour) {
if (colour < 0 || colour > 0xFFFFFF) colour = -1; if (colour < 0 || colour > 0xFFFFFF) colour = -1;
if (lightColour == colour) return;
lightColour = colour; lightColour = colour;
lightChanged = true;
} }
@Override @Override
@ -151,9 +152,11 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
tracking.removeIf(player -> !player.isAlive() || player.level() != getLevel()); 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. // And now find any new players, add them to the tracking list, and broadcast state where appropriate.
var sendState = hasOutputChanged() || lightChanged; var state = getState();
lightChanged = false; if (oldLightColour != lightColour || oldComputerState != state) {
if (sendState) { oldComputerState = state;
oldLightColour = lightColour;
// Broadcast the state to all players // Broadcast the state to all players
tracking.addAll(getLevel().players()); tracking.addAll(getLevel().players());
ServerNetworking.sendToPlayers(new PocketComputerDataMessage(this, false), tracking); ServerNetworking.sendToPlayers(new PocketComputerDataMessage(this, false), tracking);
@ -182,6 +185,6 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
@Override @Override
protected void onRemoved() { protected void onRemoved() {
super.onRemoved(); super.onRemoved();
ServerNetworking.sendToAllPlayers(new PocketComputerDeletedClientMessage(getInstanceID()), getLevel().getServer()); ServerNetworking.sendToAllPlayers(new PocketComputerDeletedClientMessage(getInstanceUUID()), getLevel().getServer());
} }
} }

View File

@ -43,6 +43,7 @@ import net.minecraft.world.level.Level;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
public class PocketComputerItem extends Item implements IComputerItem, IMedia, IColouredItem { public class PocketComputerItem extends Item implements IComputerItem, IMedia, IColouredItem {
private static final String NBT_UPGRADE = "Upgrade"; 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) { public PocketServerComputer createServerComputer(ServerLevel level, Entity entity, @Nullable Container inventory, ItemStack stack) {
var sessionID = getSessionID(stack);
var registry = ServerContext.get(level.getServer()).registry(); 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) { if (computer == null) {
var computerID = getComputerID(stack); var computerID = getComputerID(stack);
if (computerID < 0) { 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()); computer = new PocketServerComputer(level, entity.blockPosition(), getComputerID(stack), getLabel(stack), getFamily());
setInstanceID(stack, computer.register()); var tag = stack.getOrCreateTag();
setSessionID(stack, registry.getSessionID()); tag.putInt(NBT_SESSION, registry.getSessionID());
tag.putUUID(NBT_INSTANCE, computer.register());
var upgrade = getUpgrade(stack); var upgrade = getUpgrade(stack);
@ -267,13 +268,9 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
return null; return null;
} }
public static int getInstanceID(ItemStack stack) { public static @Nullable UUID getInstanceID(ItemStack stack) {
var nbt = stack.getTag(); var nbt = stack.getTag();
return nbt != null && nbt.contains(NBT_INSTANCE) ? nbt.getInt(NBT_INSTANCE) : -1; return nbt != null && nbt.hasUUID(NBT_INSTANCE) ? nbt.getUUID(NBT_INSTANCE) : null;
}
private static void setInstanceID(ItemStack stack, int instanceID) {
stack.getOrCreateTag().putInt(NBT_INSTANCE, instanceID);
} }
private static int getSessionID(ItemStack stack) { 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; 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) { private static boolean isMarkedOn(ItemStack stack) {
var nbt = stack.getTag(); var nbt = stack.getTag();
return nbt != null && nbt.getBoolean(NBT_ON); return nbt != null && nbt.getBoolean(NBT_ON);

View File

@ -25,10 +25,9 @@ import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.core.NonNullList; import net.minecraft.core.NonNullList;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.ContainerHelper;
import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.AbstractContainerMenu;
@ -136,17 +135,8 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba
super.loadServer(nbt); super.loadServer(nbt);
// Read inventory // Read inventory
var nbttaglist = nbt.getList("Items", Tag.TAG_COMPOUND); ContainerHelper.loadAllItems(nbt, inventory);
inventory.clear(); for (var i = 0; i < inventory.size(); i++) inventorySnapshot.set(i, inventory.get(i).copy());
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());
}
}
// Read state // Read state
brain.readFromNBT(nbt); brain.readFromNBT(nbt);
@ -155,16 +145,7 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba
@Override @Override
public void saveAdditional(CompoundTag nbt) { public void saveAdditional(CompoundTag nbt) {
// Write inventory // Write inventory
var nbttaglist = new ListTag(); ContainerHelper.saveAllItems(nbt, inventory);
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);
// Write brain // Write brain
nbt = brain.writeToNBT(nbt); nbt = brain.writeToNBT(nbt);
@ -186,7 +167,7 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba
if (dir.getAxis() == Direction.Axis.Y) dir = Direction.NORTH; if (dir.getAxis() == Direction.Axis.Y) dir = Direction.NORTH;
level.setBlockAndUpdate(worldPosition, getBlockState().setValue(TurtleBlock.FACING, dir)); level.setBlockAndUpdate(worldPosition, getBlockState().setValue(TurtleBlock.FACING, dir));
updateOutput(); updateRedstone();
updateInputsImmediately(); updateInputsImmediately();
onTileEntityChange(); onTileEntityChange();

View File

@ -14,6 +14,7 @@ import dan200.computercraft.api.turtle.TurtleCommand;
import dan200.computercraft.api.turtle.TurtleSide; import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.core.computer.ComputerSide; import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.util.PeripheralHelpers;
import dan200.computercraft.impl.TurtleUpgrades; import dan200.computercraft.impl.TurtleUpgrades;
import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer; import dan200.computercraft.shared.computer.core.ServerComputer;
@ -296,7 +297,7 @@ public class TurtleBrain implements TurtleAccessInternal {
oldWorld.removeBlock(oldPos, false); oldWorld.removeBlock(oldPos, false);
// Make sure everybody knows about it // Make sure everybody knows about it
newTurtle.updateOutput(); newTurtle.updateRedstone();
newTurtle.updateInputsImmediately(); newTurtle.updateInputsImmediately();
return true; return true;
} }
@ -589,7 +590,7 @@ public class TurtleBrain implements TurtleAccessInternal {
} }
var existing = peripherals.get(side); 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. // If the peripheral is the same, just use that.
peripheral = existing; peripheral = existing;
} else { } else {

View File

@ -54,7 +54,7 @@ public class TurtleSuckCommand implements TurtleCommand {
case ContainerTransfer.NO_SPACE: case ContainerTransfer.NO_SPACE:
return TurtleCommandResult.failure("No space for items"); return TurtleCommandResult.failure("No space for items");
case ContainerTransfer.NO_ITEMS: case ContainerTransfer.NO_ITEMS:
return TurtleCommandResult.failure("No items to drop"); return TurtleCommandResult.failure("No items to take");
default: default:
turtle.playAnimation(TurtleAnimation.WAIT); turtle.playAnimation(TurtleAnimation.WAIT);
return TurtleCommandResult.success(); return TurtleCommandResult.success();

View File

@ -11,6 +11,11 @@ public final class DirectionUtil {
private 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 final Direction[] FACINGS = Direction.values();
public static ComputerSide toLocal(Direction front, Direction dir) { public static ComputerSide toLocal(Direction front, Direction dir) {
@ -31,4 +36,15 @@ public final class DirectionUtil {
default -> 0.0f; 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;
}
} }

View File

@ -5,13 +5,20 @@
package dan200.computercraft.shared.util; package dan200.computercraft.shared.util;
import net.minecraft.core.BlockPos; 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.LevelAccessor;
import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity; 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.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/** /**
* A thread-safe version of {@link LevelAccessor#scheduleTick(BlockPos, Block, int)}. * A thread-safe version of {@link LevelAccessor#scheduleTick(BlockPos, Block, int)}.
@ -22,26 +29,92 @@ public final class TickScheduler {
private TickScheduler() { private TickScheduler() {
} }
/**
* The list of block entities to tick.
*/
private static final Queue<Token> toTick = new ConcurrentLinkedDeque<>(); 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) { public static void schedule(Token token) {
var world = token.owner.getLevel(); 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() { public static void tick() {
Token token; Token token;
while ((token = toTick.poll()) != null) { while ((token = toTick.poll()) != null) Token.STATE.set(token, tickToken(token));
token.scheduled.set(false); }
var blockEntity = token.owner;
if (blockEntity.isRemoved()) continue;
var world = blockEntity.getLevel(); private static State tickToken(Token token) {
var pos = blockEntity.getBlockPos(); var blockEntity = token.owner;
if (world != null && world.isLoaded(pos) && world.getBlockEntity(pos) == blockEntity) { // If the block entity has been removed, then remove it from the queue.
world.scheduleTick(pos, blockEntity.getBlockState().getBlock(), 0); 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. * As such, it should be unique per {@link BlockEntity} instance to avoid it being queued multiple times.
*/ */
public static class Token { public static class Token {
static final AtomicReferenceFieldUpdater<Token, State> STATE = AtomicReferenceFieldUpdater.newUpdater(Token.class, State.class, "$state");
final BlockEntity owner; final BlockEntity owner;
final AtomicBoolean scheduled = new AtomicBoolean();
/**
* The current state of this token.
*/
private volatile State $state = State.IDLE;
public Token(BlockEntity owner) { public Token(BlockEntity owner) {
this.owner = 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);
}
} }

View File

@ -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 # Additional access wideners for vanilla code. This is a effectively the subset of Fabric's transitive access wideners
# that we actually use # 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 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 class net/minecraft/world/item/CreativeModeTab$Output
accessible field net/minecraft/world/item/CreativeModeTabs OP_BLOCKS Lnet/minecraft/resources/ResourceKey; accessible field net/minecraft/world/item/CreativeModeTabs OP_BLOCKS Lnet/minecraft/resources/ResourceKey;

View File

@ -4,6 +4,7 @@
package dan200.computercraft.client.sound; package dan200.computercraft.client.sound;
import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -16,7 +17,7 @@ public class DfpwmStreamTest {
var stream = new DfpwmStream(); 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 }); 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); var buffer = stream.read(1024 + 1);
assertEquals(1024, buffer.remaining(), "Must have read 1024 bytes"); assertEquals(1024, buffer.remaining(), "Must have read 1024 bytes");

View File

@ -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));
}
}
}
}
}
}

View File

@ -9,19 +9,14 @@ import dan200.computercraft.api.network.wired.WiredNetwork;
import dan200.computercraft.api.network.wired.WiredNetworkChange; import dan200.computercraft.api.network.wired.WiredNetworkChange;
import dan200.computercraft.api.network.wired.WiredNode; import dan200.computercraft.api.network.wired.WiredNode;
import dan200.computercraft.api.peripheral.IPeripheral; 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.level.Level;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@ -29,11 +24,11 @@ public class NetworkTest {
@Test @Test
public void testConnect() { public void testConnect() {
NetworkElement NetworkElement
aE = new NetworkElement(null, null, "a"), aE = new NetworkElement("a"),
bE = new NetworkElement(null, null, "b"), bE = new NetworkElement("b"),
cE = new NetworkElement(null, null, "c"); cE = new NetworkElement("c");
WiredNode WiredNodeImpl
aN = aE.getNode(), aN = aE.getNode(),
bN = bE.getNode(), bN = bE.getNode(),
cN = cE.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(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"); 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"); assertTrue(aN.connectTo(bN), "Must be able to add connection");
assertFalse(aN.getNetwork().connect(aN, bN), "Cannot add connection twice"); assertFalse(aN.connectTo(bN), "Cannot add connection twice");
assertEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must be equal"); 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"); 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"), 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"); 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(), bN.getNetwork(), "A's and B's network must be equal");
assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal"); assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal");
@ -69,20 +64,20 @@ public class NetworkTest {
@Test @Test
public void testDisconnectNoChange() { public void testDisconnectNoChange() {
NetworkElement NetworkElement
aE = new NetworkElement(null, null, "a"), aE = new NetworkElement("a"),
bE = new NetworkElement(null, null, "b"), bE = new NetworkElement("b"),
cE = new NetworkElement(null, null, "c"); cE = new NetworkElement("c");
WiredNode WiredNodeImpl
aN = aE.getNode(), aN = aE.getNode(),
bN = bE.getNode(), bN = bE.getNode(),
cN = cE.getNode(); cN = cE.getNode();
aN.getNetwork().connect(aN, bN); aN.connectTo(bN);
aN.getNetwork().connect(aN, cN); aN.connectTo(cN);
aN.getNetwork().connect(bN, 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(), bN.getNetwork(), "A's and B's network must be equal");
assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal"); assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal");
@ -96,19 +91,19 @@ public class NetworkTest {
@Test @Test
public void testDisconnectLeaf() { public void testDisconnectLeaf() {
NetworkElement NetworkElement
aE = new NetworkElement(null, null, "a"), aE = new NetworkElement("a"),
bE = new NetworkElement(null, null, "b"), bE = new NetworkElement("b"),
cE = new NetworkElement(null, null, "c"); cE = new NetworkElement("c");
WiredNode WiredNodeImpl
aN = aE.getNode(), aN = aE.getNode(),
bN = bE.getNode(), bN = bE.getNode(),
cN = cE.getNode(); cN = cE.getNode();
aN.getNetwork().connect(aN, bN); aN.connectTo(bN);
aN.getNetwork().connect(aN, cN); 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"); assertNotEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal");
assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal"); assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal");
@ -123,23 +118,23 @@ public class NetworkTest {
@Test @Test
public void testDisconnectSplit() { public void testDisconnectSplit() {
NetworkElement NetworkElement
aE = new NetworkElement(null, null, "a"), aE = new NetworkElement("a"),
aaE = new NetworkElement(null, null, "a_"), aaE = new NetworkElement("a_"),
bE = new NetworkElement(null, null, "b"), bE = new NetworkElement("b"),
bbE = new NetworkElement(null, null, "b_"); bbE = new NetworkElement("b_");
WiredNode WiredNodeImpl
aN = aE.getNode(), aN = aE.getNode(),
aaN = aaE.getNode(), aaN = aaE.getNode(),
bN = bE.getNode(), bN = bE.getNode(),
bbN = bbE.getNode(); bbN = bbE.getNode();
aN.getNetwork().connect(aN, aaN); aN.connectTo(aaN);
bN.getNetwork().connect(bN, bbN); 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"); 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"); assertEquals(aN.getNetwork(), aaN.getNetwork(), "A's and A_'s network must be equal");
@ -154,7 +149,7 @@ public class NetworkTest {
@Test @Test
public void testRemoveSingle() { public void testRemoveSingle() {
var aE = new NetworkElement(null, null, "a"); var aE = new NetworkElement("a");
var aN = aE.getNode(); var aN = aE.getNode();
var network = aN.getNetwork(); var network = aN.getNetwork();
@ -165,20 +160,20 @@ public class NetworkTest {
@Test @Test
public void testRemoveLeaf() { public void testRemoveLeaf() {
NetworkElement NetworkElement
aE = new NetworkElement(null, null, "a"), aE = new NetworkElement("a"),
bE = new NetworkElement(null, null, "b"), bE = new NetworkElement("b"),
cE = new NetworkElement(null, null, "c"); cE = new NetworkElement("c");
WiredNode WiredNodeImpl
aN = aE.getNode(), aN = aE.getNode(),
bN = bE.getNode(), bN = bE.getNode(),
cN = cE.getNode(); cN = cE.getNode();
aN.getNetwork().connect(aN, bN); aN.connectTo(bN);
aN.getNetwork().connect(aN, cN); aN.connectTo(cN);
assertTrue(aN.getNetwork().remove(bN), "Must be able to remove node"); assertTrue(bN.remove(), "Must be able to remove node");
assertFalse(aN.getNetwork().remove(bN), "Cannot remove a second time"); assertFalse(bN.remove(), "Cannot remove a second time");
assertNotEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal"); assertNotEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal");
assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal"); assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal");
@ -194,26 +189,26 @@ public class NetworkTest {
@Test @Test
public void testRemoveSplit() { public void testRemoveSplit() {
NetworkElement NetworkElement
aE = new NetworkElement(null, null, "a"), aE = new NetworkElement("a"),
aaE = new NetworkElement(null, null, "a_"), aaE = new NetworkElement("a_"),
bE = new NetworkElement(null, null, "b"), bE = new NetworkElement("b"),
bbE = new NetworkElement(null, null, "b_"), bbE = new NetworkElement("b_"),
cE = new NetworkElement(null, null, "c"); cE = new NetworkElement("c");
WiredNode WiredNodeImpl
aN = aE.getNode(), aN = aE.getNode(),
aaN = aaE.getNode(), aaN = aaE.getNode(),
bN = bE.getNode(), bN = bE.getNode(),
bbN = bbE.getNode(), bbN = bbE.getNode(),
cN = cE.getNode(); cN = cE.getNode();
aN.getNetwork().connect(aN, aaN); aN.connectTo(aaN);
bN.getNetwork().connect(bN, bbN); bN.connectTo(bbN);
cN.getNetwork().connect(aN, cN); cN.connectTo(aN);
cN.getNetwork().connect(bN, cN); cN.connectTo(bN);
cN.getNetwork().remove(cN); cN.remove();
assertNotEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal"); 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"); 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"); assertEquals(Set.of(), cE.allPeripherals().keySet(), "C's peripheral set should be empty");
} }
private static final int BRUTE_SIZE = 16; static final class NetworkElement implements WiredElement {
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;
private final String id; 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> localPeripherals = new HashMap<>();
private final Map<String, IPeripheral> remotePeripherals = new HashMap<>(); private final Map<String, IPeripheral> remotePeripherals = new HashMap<>();
private NetworkElement(Level world, Vec3 position, String id) { NetworkElement(String id) {
this.world = world; this(id, true);
this.position = position; }
NetworkElement(String id, boolean peripheral) {
this.id = id; this.id = id;
this.node = new WiredNodeImpl(this); this.node = new WiredNodeImpl(this);
this.addPeripheral(id); if (peripheral) addPeripheral(id);
} }
@Override @Override
public Level getLevel() { public Level getLevel() {
return world; throw new IllegalStateException("Unexpected call to getLevel()");
} }
@Override @Override
public Vec3 getPosition() { public Vec3 getPosition() {
return position; throw new IllegalStateException("Unexpected call to getPosition()");
} }
@Override @Override
@ -331,7 +260,7 @@ public class NetworkTest {
} }
@Override @Override
public WiredNode getNode() { public WiredNodeImpl getNode() {
return node; 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) { private static Set<WiredNodeImpl> nodes(WiredNetwork network) {
return ((WiredNetworkImpl) network).nodes; return ((WiredNetworkImpl) network).nodes;
} }

View File

@ -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();
}
}

View File

@ -23,7 +23,7 @@ class DfpwmStateTest {
var state = new DfpwmState(); var state = new DfpwmState();
state.pushBuffer(new ObjectLuaTable(inputTbl), input.length, Optional.empty()); 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()]; var contents = new byte[result.remaining()];
result.get(contents); result.get(contents);

View File

@ -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);
}
}

View File

@ -11,11 +11,15 @@ import dan200.computercraft.core.apis.TermAPI
import dan200.computercraft.core.computer.ComputerSide import dan200.computercraft.core.computer.ComputerSide
import dan200.computercraft.gametest.api.* import dan200.computercraft.gametest.api.*
import dan200.computercraft.shared.ModRegistry import dan200.computercraft.shared.ModRegistry
import dan200.computercraft.test.core.assertArrayEquals
import dan200.computercraft.test.core.computer.getApi import dan200.computercraft.test.core.computer.getApi
import net.minecraft.core.BlockPos import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.gametest.framework.GameTest import net.minecraft.gametest.framework.GameTest
import net.minecraft.gametest.framework.GameTestHelper import net.minecraft.gametest.framework.GameTestHelper
import net.minecraft.world.InteractionHand 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.Blocks
import net.minecraft.world.level.block.LeverBlock import net.minecraft.world.level.block.LeverBlock
import net.minecraft.world.level.block.RedstoneLampBlock 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. * Check the client can open the computer UI and interact with it.
*/ */

View File

@ -7,18 +7,21 @@ package dan200.computercraft.gametest
import dan200.computercraft.api.lua.ObjectArguments import dan200.computercraft.api.lua.ObjectArguments
import dan200.computercraft.core.apis.PeripheralAPI import dan200.computercraft.core.apis.PeripheralAPI
import dan200.computercraft.core.computer.ComputerSide import dan200.computercraft.core.computer.ComputerSide
import dan200.computercraft.gametest.api.getBlockEntity import dan200.computercraft.gametest.api.*
import dan200.computercraft.gametest.api.sequence import dan200.computercraft.impl.network.wired.WiredNodeImpl
import dan200.computercraft.gametest.api.thenOnComputer
import dan200.computercraft.gametest.api.thenStartComputer
import dan200.computercraft.shared.ModRegistry import dan200.computercraft.shared.ModRegistry
import dan200.computercraft.shared.peripheral.modem.wired.CableBlock 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.assertArrayEquals
import dan200.computercraft.test.core.computer.LuaTaskContext import dan200.computercraft.test.core.computer.LuaTaskContext
import dan200.computercraft.test.core.computer.getApi import dan200.computercraft.test.core.computer.getApi
import net.minecraft.core.BlockPos import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.gametest.framework.GameTest import net.minecraft.gametest.framework.GameTest
import net.minecraft.gametest.framework.GameTestHelper 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 org.junit.jupiter.api.Assertions.assertEquals
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@ -83,7 +86,86 @@ class Modem_Test {
thenExecute { thenExecute {
val modem1 = helper.getBlockEntity(BlockPos(1, 2, 1), ModRegistry.BlockEntities.WIRED_MODEM_FULL.get()) 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()) 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 if (!peripheral.isPresent(side)) continue
peripherals.add(side) peripherals.add(side)
val hasType = peripheral.hasType(side, "modem") val hasType = peripheral.hasType(side, "peripheral_hub")
if (hasType == null || hasType[0] != true) continue if (hasType == null || hasType[0] != true) continue
val names = peripheral.call(context, ObjectArguments(side, "getNamesRemote")).await() ?: continue val names = peripheral.call(context, ObjectArguments(side, "getNamesRemote")).await() ?: continue
@ -116,3 +198,22 @@ private suspend fun LuaTaskContext.getPeripheralNames(): List<String> {
peripherals.sort() peripherals.sort()
return peripherals 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")
}

View File

@ -38,7 +38,7 @@ class Pocket_Computer_Test {
// And ensure its synced to the client. // And ensure its synced to the client.
thenIdle(4) thenIdle(4)
thenOnClient { thenOnClient {
val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem) val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)!!
assertEquals(ComputerState.ON, pocketComputer.state) assertEquals(ComputerState.ON, pocketComputer.state)
val term = pocketComputer.terminal val term = pocketComputer.terminal
@ -54,7 +54,7 @@ class Pocket_Computer_Test {
// And ensure the new computer state and terminal are sent. // And ensure the new computer state and terminal are sent.
thenIdle(4) thenIdle(4)
thenOnClient { thenOnClient {
val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem) val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)!!
assertEquals(ComputerState.BLINKING, pocketComputer.state) assertEquals(ComputerState.BLINKING, pocketComputer.state)
val term = pocketComputer.terminal val term = pocketComputer.terminal

View File

@ -329,8 +329,6 @@ class Turtle_Test {
/** /**
* Checks turtles can be cleaned in cauldrons. * Checks turtles can be cleaned in cauldrons.
*
* Currently not required as turtles can no longer right-click cauldrons.
*/ */
@GameTest @GameTest
fun Cleaned_with_cauldrons(helper: GameTestHelper) = helper.sequence { 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. * Render turtles as an item.

View File

@ -20,15 +20,19 @@ import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.gametest.framework.* import net.minecraft.gametest.framework.*
import net.minecraft.resources.ResourceLocation import net.minecraft.resources.ResourceLocation
import net.minecraft.world.Container import net.minecraft.world.Container
import net.minecraft.world.InteractionHand
import net.minecraft.world.entity.Entity import net.minecraft.world.entity.Entity
import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.EntityType
import net.minecraft.world.item.ItemStack 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.Blocks
import net.minecraft.world.level.block.entity.BarrelBlockEntity import net.minecraft.world.level.block.entity.BarrelBlockEntity
import net.minecraft.world.level.block.entity.BlockEntity import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.entity.BlockEntityType import net.minecraft.world.level.block.entity.BlockEntityType
import net.minecraft.world.level.block.state.BlockState import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.properties.Property 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.Matchers
import org.hamcrest.StringDescription import org.hamcrest.StringDescription
@ -306,3 +310,16 @@ fun GameTestHelper.setContainerItem(pos: BlockPos, slot: Int, item: ItemStack) {
container.setItem(slot, item) container.setItem(slot, item)
container.setChanged() 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))
}

View 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}"
]
}

View File

@ -0,0 +1,139 @@
{
DataVersion: 3465,
size: [5, 5, 5],
data: [
{pos: [0, 0, 0], state: "minecraft:polished_andesite"},
{pos: [0, 0, 1], state: "minecraft:polished_andesite"},
{pos: [0, 0, 2], state: "minecraft:polished_andesite"},
{pos: [0, 0, 3], state: "minecraft:polished_andesite"},
{pos: [0, 0, 4], state: "minecraft:polished_andesite"},
{pos: [1, 0, 0], state: "minecraft:polished_andesite"},
{pos: [1, 0, 1], state: "minecraft:polished_andesite"},
{pos: [1, 0, 2], state: "minecraft:polished_andesite"},
{pos: [1, 0, 3], state: "minecraft:polished_andesite"},
{pos: [1, 0, 4], state: "minecraft:polished_andesite"},
{pos: [2, 0, 0], state: "minecraft:polished_andesite"},
{pos: [2, 0, 1], state: "minecraft:polished_andesite"},
{pos: [2, 0, 2], state: "minecraft:polished_andesite"},
{pos: [2, 0, 3], state: "minecraft:polished_andesite"},
{pos: [2, 0, 4], state: "minecraft:polished_andesite"},
{pos: [3, 0, 0], state: "minecraft:polished_andesite"},
{pos: [3, 0, 1], state: "minecraft:polished_andesite"},
{pos: [3, 0, 2], state: "minecraft:polished_andesite"},
{pos: [3, 0, 3], state: "minecraft:polished_andesite"},
{pos: [3, 0, 4], state: "minecraft:polished_andesite"},
{pos: [4, 0, 0], state: "minecraft:polished_andesite"},
{pos: [4, 0, 1], state: "minecraft:polished_andesite"},
{pos: [4, 0, 2], state: "minecraft:polished_andesite"},
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
{pos: [0, 1, 0], state: "minecraft:air"},
{pos: [0, 1, 1], state: "minecraft:air"},
{pos: [0, 1, 2], state: "minecraft:air"},
{pos: [0, 1, 3], state: "minecraft:air"},
{pos: [0, 1, 4], state: "minecraft:air"},
{pos: [1, 1, 0], state: "minecraft:air"},
{pos: [1, 1, 1], state: "minecraft:air"},
{pos: [1, 1, 2], state: "computercraft: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}"
]
}

View 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}"
]
}

View File

@ -0,0 +1,139 @@
{
DataVersion: 3465,
size: [5, 5, 5],
data: [
{pos: [0, 0, 0], state: "minecraft:polished_andesite"},
{pos: [0, 0, 1], state: "minecraft:polished_andesite"},
{pos: [0, 0, 2], state: "minecraft:polished_andesite"},
{pos: [0, 0, 3], state: "minecraft:polished_andesite"},
{pos: [0, 0, 4], state: "minecraft:polished_andesite"},
{pos: [1, 0, 0], state: "minecraft:polished_andesite"},
{pos: [1, 0, 1], state: "minecraft:polished_andesite"},
{pos: [1, 0, 2], state: "minecraft:polished_andesite"},
{pos: [1, 0, 3], state: "minecraft:polished_andesite"},
{pos: [1, 0, 4], state: "minecraft:polished_andesite"},
{pos: [2, 0, 0], state: "minecraft:polished_andesite"},
{pos: [2, 0, 1], state: "minecraft:polished_andesite"},
{pos: [2, 0, 2], state: "minecraft:polished_andesite"},
{pos: [2, 0, 3], state: "minecraft:polished_andesite"},
{pos: [2, 0, 4], state: "minecraft:polished_andesite"},
{pos: [3, 0, 0], state: "minecraft:polished_andesite"},
{pos: [3, 0, 1], state: "minecraft:polished_andesite"},
{pos: [3, 0, 2], state: "minecraft:polished_andesite"},
{pos: [3, 0, 3], state: "minecraft:polished_andesite"},
{pos: [3, 0, 4], state: "minecraft:polished_andesite"},
{pos: [4, 0, 0], state: "minecraft:polished_andesite"},
{pos: [4, 0, 1], state: "minecraft:polished_andesite"},
{pos: [4, 0, 2], state: "minecraft:polished_andesite"},
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
{pos: [0, 1, 0], state: "minecraft:air"},
{pos: [0, 1, 1], state: "minecraft:air"},
{pos: [0, 1, 2], state: "minecraft:air"},
{pos: [0, 1, 3], state: "minecraft:air"},
{pos: [0, 1, 4], state: "minecraft:air"},
{pos: [1, 1, 0], state: "minecraft:air"},
{pos: [1, 1, 1], state: "minecraft:air"},
{pos: [1, 1, 2], state: "computercraft: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}"
]
}

View File

@ -28,7 +28,7 @@
{pos: [4, 0, 3], state: "minecraft:polished_andesite"}, {pos: [4, 0, 3], state: "minecraft:polished_andesite"},
{pos: [4, 0, 4], 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, 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, 2], state: "minecraft:air"},
{pos: [0, 1, 3], 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"}}, {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, 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, 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, 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, 0], state: "minecraft:air"},
{pos: [2, 1, 1], state: "minecraft:air"}, {pos: [2, 1, 1], state: "minecraft:air"},
{pos: [2, 1, 2], state: "minecraft:air"}, {pos: [2, 1, 2], state: "minecraft:air"},
@ -133,11 +133,11 @@
"minecraft:polished_andesite", "minecraft:polished_andesite",
"minecraft:air", "minecraft:air",
"computercraft:printer{bottom:false,facing:north,top:false}", "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: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: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: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: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: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}" "computercraft:cable{cable:true,down:false,east:false,modem:north_off,north:true,south:false,up:false,waterlogged:false,west:true}"

View File

@ -28,7 +28,7 @@
{pos: [4, 0, 3], state: "minecraft:polished_andesite"}, {pos: [4, 0, 3], state: "minecraft:polished_andesite"},
{pos: [4, 0, 4], 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, 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, 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, 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"}}, {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, 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, 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, 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, 0], state: "minecraft:air"},
{pos: [2, 1, 1], 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"}}, {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:polished_andesite",
"minecraft:air", "minecraft:air",
"computercraft:printer{bottom:false,facing:north,top:false}", "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: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: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: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: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: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: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}" "computercraft:computer_advanced{facing:north,state:blinking}"

View 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: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}"
]
}

View 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: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}"
]
}

View 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}"
]
}

View File

@ -116,6 +116,7 @@ public final class MethodResult {
* @return The method result which represents this yield. * @return The method result which represents this yield.
* @see #pullEvent(String, ILuaCallback) * @see #pullEvent(String, ILuaCallback)
*/ */
@SuppressWarnings("NamedLikeContextualKeyword")
public static MethodResult yield(@Nullable Object[] arguments, ILuaCallback callback) { public static MethodResult yield(@Nullable Object[] arguments, ILuaCallback callback) {
Objects.requireNonNull(callback, "callback cannot be null"); Objects.requireNonNull(callback, "callback cannot be null");
return new MethodResult(arguments, callback); return new MethodResult(arguments, callback);

View File

@ -11,6 +11,7 @@ import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.peripheral.NotAttachedException; import dan200.computercraft.api.peripheral.NotAttachedException;
import dan200.computercraft.api.peripheral.WorkMonitor; import dan200.computercraft.api.peripheral.WorkMonitor;
import dan200.computercraft.core.computer.ComputerSide; import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.computer.GuardedLuaContext;
import dan200.computercraft.core.methods.MethodSupplier; import dan200.computercraft.core.methods.MethodSupplier;
import dan200.computercraft.core.methods.PeripheralMethod; import dan200.computercraft.core.methods.PeripheralMethod;
import dan200.computercraft.core.metrics.Metrics; import dan200.computercraft.core.metrics.Metrics;
@ -26,7 +27,7 @@ import java.util.*;
* @hidden * @hidden
*/ */
public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChangeListener { 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 String side;
private final IPeripheral peripheral; private final IPeripheral peripheral;
@ -35,6 +36,8 @@ public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange
private final Map<String, PeripheralMethod> methodMap; private final Map<String, PeripheralMethod> methodMap;
private boolean attached = false; private boolean attached = false;
private @Nullable GuardedLuaContext contextWrapper;
PeripheralWrapper(IPeripheral peripheral, String side) { PeripheralWrapper(IPeripheral peripheral, String side) {
super(environment); super(environment);
this.side = side; this.side = side;
@ -91,9 +94,21 @@ public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange
if (method == null) throw new LuaException("No such method " + methodName); if (method == null) throw new LuaException("No such method " + methodName);
try (var ignored = environment.time(Metrics.PERIPHERAL_OPS)) { // Wrap the ILuaContext. We try to reuse the previous context where possible to avoid allocations - this
return method.apply(peripheral, context, this, arguments); // 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 // IComputerAccess implementation

View File

@ -13,8 +13,54 @@ import dan200.computercraft.core.util.Colour;
/** /**
* Interact with a computer's terminal or monitors, writing text and drawing * Interact with a computer's terminal or monitors, writing text and drawing ASCII graphics.
* 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 * @cc.module term
*/ */

View File

@ -4,6 +4,7 @@
package dan200.computercraft.core.apis.http.options; 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. * 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 websocketMessage The maximum size of a websocket message (outgoing and incoming).
* @param useProxy Whether to use the configured proxy. * @param useProxy Whether to use the configured proxy.
*/ */
@Immutable
public record Options(Action action, long maxUpload, long maxDownload, int websocketMessage, boolean useProxy) { public record Options(Action action, long maxUpload, long maxDownload, int websocketMessage, boolean useProxy) {
} }

View File

@ -5,6 +5,7 @@
package dan200.computercraft.core.apis.http.options; package dan200.computercraft.core.apis.http.options;
import com.google.errorprone.annotations.Immutable; import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.annotations.concurrent.LazyInit;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Optional; import java.util.Optional;
@ -23,7 +24,7 @@ public final class PartialOptions {
private final OptionalInt websocketMessage; private final OptionalInt websocketMessage;
private final Optional<Boolean> useProxy; private final Optional<Boolean> useProxy;
@SuppressWarnings("Immutable") // Lazily initialised, so this mutation is invisible in the public API @LazyInit
private @Nullable Options options; private @Nullable Options options;
public PartialOptions(@Nullable Action action, OptionalLong maxUpload, OptionalLong maxDownload, OptionalInt websocketMessage, Optional<Boolean> useProxy) { public PartialOptions(@Nullable Action action, OptionalLong maxUpload, OptionalLong maxDownload, OptionalInt websocketMessage, Optional<Boolean> useProxy) {

View File

@ -27,7 +27,7 @@ final class ResultHelpers {
return throwUnchecked0(t); return throwUnchecked0(t);
} }
@SuppressWarnings("unchecked") @SuppressWarnings({ "unchecked", "TypeParameterUnusedInFormals" })
private static <T extends Throwable> T throwUnchecked0(Throwable t) throws T { private static <T extends Throwable> T throwUnchecked0(Throwable t) throws T {
throw (T) t; throw (T) t;
} }

View File

@ -15,8 +15,7 @@ import dan200.computercraft.core.filesystem.FileSystem;
import dan200.computercraft.core.terminal.Terminal; import dan200.computercraft.core.terminal.Terminal;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
/** /**
@ -54,9 +53,9 @@ public class Computer {
private final AtomicLong lastTaskId = new AtomicLong(); private final AtomicLong lastTaskId = new AtomicLong();
// Additional state about the computer and its environment. // Additional state about the computer and its environment.
private boolean blinking = false;
private final Environment internalEnvironment; private final Environment internalEnvironment;
private final AtomicBoolean externalOutputChanged = new AtomicBoolean();
private final AtomicInteger externalOutputChanges = new AtomicInteger();
private boolean startRequested; private boolean startRequested;
private int ticksSinceStart = -1; private int ticksSinceStart = -1;
@ -140,10 +139,7 @@ public class Computer {
} }
public void setLabel(@Nullable String label) { public void setLabel(@Nullable String label) {
if (!Objects.equals(label, this.label)) { this.label = label;
this.label = label;
externalOutputChanged.set(true);
}
} }
public void tick() { public void tick() {
@ -164,28 +160,24 @@ public class Computer {
internalEnvironment.tick(); internalEnvironment.tick();
// Propagate the environment's output to the world. // Propagate the environment's output to the world.
if (internalEnvironment.updateOutput()) externalOutputChanged.set(true); externalOutputChanges.accumulateAndGet(internalEnvironment.updateOutput(), (x, y) -> x | y);
// 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);
}
} }
void markChanged() { /**
externalOutputChanged.set(true); * Get a bitmask returning which sides on the computer have changed, resetting the internal state.
} *
* @return What sides on the computer have changed.
public boolean pollAndResetChanged() { */
return externalOutputChanged.getAndSet(false); public int pollAndResetChanges() {
return externalOutputChanges.getAndSet(0);
} }
public boolean isBlinking() { 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) { public void addApi(ILuaAPI api) {

Some files were not shown because too many files have changed in this diff Show More