mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-11-04 15:43:00 +00:00 
			
		
		
		
	Merge branch 'mc-1.20.x' into mc-1.20.y
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -9,6 +9,7 @@
 | 
			
		||||
/projects/*/logs
 | 
			
		||||
/projects/fabric/fabricloader.log
 | 
			
		||||
/projects/*/build
 | 
			
		||||
/projects/*/src/test/generated_tests/
 | 
			
		||||
/buildSrc/build
 | 
			
		||||
/out
 | 
			
		||||
/buildSrc/out
 | 
			
		||||
 
 | 
			
		||||
@@ -102,6 +102,8 @@ sourceSets.all {
 | 
			
		||||
            option("NullAway:CastToNonNullMethod", "dan200.computercraft.core.util.Nullability.assertNonNull")
 | 
			
		||||
            option("NullAway:CheckOptionalEmptiness")
 | 
			
		||||
            option("NullAway:AcknowledgeRestrictiveAnnotations")
 | 
			
		||||
 | 
			
		||||
            excludedPaths = ".*/jmh_generated/.*"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@ val publishCurseForge by tasks.registering(TaskPublishCurseForge::class) {
 | 
			
		||||
    apiToken = findProperty("curseForgeApiKey") ?: ""
 | 
			
		||||
    enabled = apiToken != ""
 | 
			
		||||
 | 
			
		||||
    val mainFile = upload("282001", modPublishing.output.get().archiveFile)
 | 
			
		||||
    val mainFile = upload("282001", modPublishing.output)
 | 
			
		||||
    mainFile.changelog =
 | 
			
		||||
        "Release notes can be found on the [GitHub repository](https://github.com/cc-tweaked/CC-Tweaked/releases/tag/v$mcVersion-$modVersion)."
 | 
			
		||||
    mainFile.changelogType = "markdown"
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,10 @@ SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
    <property name="tabWidth" value="4"/>
 | 
			
		||||
    <property name="charset" value="UTF-8" />
 | 
			
		||||
 | 
			
		||||
    <module name="BeforeExecutionExclusionFileFilter">
 | 
			
		||||
        <property name="fileNamePattern" value="module\-info\.java$"/>
 | 
			
		||||
    </module>
 | 
			
		||||
 | 
			
		||||
    <module name="SuppressionFilter">
 | 
			
		||||
        <property name="file" value="${config_loc}/suppressions.xml" />
 | 
			
		||||
    </module>
 | 
			
		||||
 
 | 
			
		||||
@@ -21,5 +21,5 @@ SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
    <suppress checks="PackageName" files=".*[\\/]T[A-Za-z]+.java" />
 | 
			
		||||
 | 
			
		||||
    <!-- Allow underscores in our test classes. -->
 | 
			
		||||
    <suppress checks="MethodName" files=".*Contract.java" />
 | 
			
		||||
    <suppress checks="MethodName" files=".*(Contract|Test).java" />
 | 
			
		||||
</suppressions>
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ In order to give the best results, a GPS constellation needs at least four compu
 | 
			
		||||
constellation is redundant, but it does not cause problems.
 | 
			
		||||
 | 
			
		||||
## Building a GPS constellation
 | 
			
		||||
<img alt="An example GPS constellation." src="/images/gps-constellation-example.png" class="big-image" />
 | 
			
		||||
<img alt="An example GPS constellation." src="../images/gps-constellation-example.png" class="big-image" />
 | 
			
		||||
 | 
			
		||||
We are going to build our GPS constellation as shown in the image above. You will need 4 computers and either 4 wireless
 | 
			
		||||
modems or 4 ender modems. Try not to mix ender and wireless modems together as you might get some odd behavior when your
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								doc/images/computercraft-dump.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								doc/images/computercraft-dump.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 254 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								doc/images/computercraft-track.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								doc/images/computercraft-track.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 304 KiB  | 
							
								
								
									
										140
									
								
								doc/reference/command.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								doc/reference/command.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,140 @@
 | 
			
		||||
---
 | 
			
		||||
module: [kind=reference] computercraft_command
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<!--
 | 
			
		||||
SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
 | 
			
		||||
SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
# The `/computercraft` command
 | 
			
		||||
CC: Tweaked provides a `/computercraft` command for server owners to manage running computers on a server.
 | 
			
		||||
 | 
			
		||||
## Permissions {#permissions}
 | 
			
		||||
As the `/computercraft` command is mostly intended for debugging and administrative purposes, its sub-commands typically
 | 
			
		||||
require you to have op (or similar).
 | 
			
		||||
 | 
			
		||||
 - All players have access to the [`queue`] sub-command.
 | 
			
		||||
 - On a multi-player server, all other commands require op.
 | 
			
		||||
 - On a single-player world, the player can run the [`dump`], [`turn-on`]/[`shutdown`], and [`track`] sub-commands, even
 | 
			
		||||
   when cheats are not enabled. The [`tp`] and [`view`] commands require cheats.
 | 
			
		||||
 | 
			
		||||
If a permission mod such as [LuckPerms] is installed[^permission], you can configure access to the individual
 | 
			
		||||
sub-commands. Each sub-command creates a `computercraft.command.NAME` permission node to control which players can
 | 
			
		||||
execute it.
 | 
			
		||||
 | 
			
		||||
[LuckPerms]: https://github.com/LuckPerms/LuckPerms/ "A permissions plugin for Minecraft servers."
 | 
			
		||||
[fabric-permission-api]: https://github.com/lucko/fabric-permissions-api "A simple permissions API for Fabric"
 | 
			
		||||
 | 
			
		||||
[^permission]: This supports any mod which uses Forge's permission API or [fabric-permission-api].
 | 
			
		||||
 | 
			
		||||
## Computer selectors {#computer-selectors}
 | 
			
		||||
Some commands (such as [`tp`] or [`turn-on`]) target a specific computer, or a list of computers. To specify which
 | 
			
		||||
computers to operate on, you must use "computer selectors".
 | 
			
		||||
 | 
			
		||||
Computer selectors are similar to Minecraft's [entity target selectors], but targeting computers instead. They allow
 | 
			
		||||
you to select one or more computers, based on a set of predicates.
 | 
			
		||||
 | 
			
		||||
The following predicates are supported:
 | 
			
		||||
 - `id=<id>`: Select computer(s) with a specific id.
 | 
			
		||||
 - `instance=<id>`: Select the computer with the given instance id.
 | 
			
		||||
 - `family=<normal|advanced|command>`: Select computers based on their type.
 | 
			
		||||
 - `label=<label>`: Select computers with the given label.
 | 
			
		||||
 - `distance=<distance>`: Select computers within a specific distance of the player executing the command. This uses
 | 
			
		||||
   Minecraft's [float range] syntax.
 | 
			
		||||
 | 
			
		||||
`#<id>` may also be used as a shorthand for `@c[id=<id>]`, to select computer(s) with a specific id.
 | 
			
		||||
 | 
			
		||||
### Examples:
 | 
			
		||||
 - `/computercraft turn-on #12`: Turn on the computer(s) with an id of 12.
 | 
			
		||||
 - `/computercraft shutdown @c[distance=..100]`: Shut down all computers with 100 blocks of the player.
 | 
			
		||||
 | 
			
		||||
[entity target selectors]: https://minecraft.wiki/w/Target_selectors "Target Selectors on the Minecraft wiki"
 | 
			
		||||
[Float range]: https://minecraft.wiki/w/Argument_types#minecraft:float_range
 | 
			
		||||
 | 
			
		||||
## Commands {#commands}
 | 
			
		||||
### `/computercraft dump` {#dump}
 | 
			
		||||
`/computercraft dump` prints a table of currently loaded computers, including their id, position, and whether they're
 | 
			
		||||
running. It can also be run with a single computer argument to dump more detailed information about a computer.
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
The table by default shows the number of times each computer has run, and how long it ran for (in total, and on
 | 
			
		||||
average). In the above screenshot, we can see one computer was particularly badly behaved, and ran for 7 seconds. The
 | 
			
		||||
buttons may be used to [teleport][`tp`] to the computer, or [open its terminal ][`view`], and inspect it further.
 | 
			
		||||
 | 
			
		||||
`/computercraft track dump` can be used to display this table at any point (including while profiling is still running).
 | 
			
		||||
 | 
			
		||||
Computers also record other information, such as how much server-thread time they consume, or their HTTP bandwidth
 | 
			
		||||
usage. The `dump` subcommand accepts a list of other fields to display, instead of the default timings.
 | 
			
		||||
 | 
			
		||||
#### Examples
 | 
			
		||||
 - `/computercraft track dump server_tasks_count server_tasks`: Print the number of server-thread tasks each computer
 | 
			
		||||
   executed, and how long they took in total.
 | 
			
		||||
 - `/computercraft track dump http_upload http_download`: Print the number of bytes uploaded and downloaded by each
 | 
			
		||||
   computer.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### `/computercraft queue` {#queue}
 | 
			
		||||
The queue subcommand allows non-operator players to queue a `computer_command` event on *command* computers.
 | 
			
		||||
 | 
			
		||||
This has a similar purpose to vanilla's [`/trigger`] command. Command computers may choose to listen to this event, and
 | 
			
		||||
then perform some action.
 | 
			
		||||
 | 
			
		||||
[`/trigger`]: https://minecraft.wiki/w/Commands/trigger "/trigger on the Minecraft wiki"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
[`dump`]: #dump "/computercraft dump"
 | 
			
		||||
[`queue`]: #queue "/computercraft queue"
 | 
			
		||||
[`shutdown`]: #shutdown "/computercraft shutdown"
 | 
			
		||||
[`tp`]: #tp "/computercraft tp"
 | 
			
		||||
[`track`]: #track "/computercraft track"
 | 
			
		||||
[`turn-on`]: #turn-on "/computercraft turn-on"
 | 
			
		||||
[`view`]: #view "/computercraft view"
 | 
			
		||||
[computer selectors]: #computer-selectors "Computer selectors"
 | 
			
		||||
@@ -10,7 +10,7 @@ kotlin.jvm.target.validation.mode=error
 | 
			
		||||
 | 
			
		||||
# Mod properties
 | 
			
		||||
isUnstable=true
 | 
			
		||||
modVersion=1.109.6
 | 
			
		||||
modVersion=1.110.0
 | 
			
		||||
 | 
			
		||||
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
 | 
			
		||||
mcVersion=1.20.4
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@
 | 
			
		||||
# Remember to update corresponding versions in fabric.mod.json/mods.toml
 | 
			
		||||
fabric-api = "0.93.1+1.20.4"
 | 
			
		||||
fabric-loader = "0.15.3"
 | 
			
		||||
neoForge = "20.4.161-beta"
 | 
			
		||||
neoForge = "20.4.210"
 | 
			
		||||
neoForgeSpi = "8.0.1"
 | 
			
		||||
mixin = "0.8.5"
 | 
			
		||||
parchment = "2023.12.31"
 | 
			
		||||
@@ -26,7 +26,7 @@ slf4j = "2.0.7"
 | 
			
		||||
asm = "9.6"
 | 
			
		||||
autoService = "1.1.1"
 | 
			
		||||
checkerFramework = "3.42.0"
 | 
			
		||||
cobalt = "0.9.1"
 | 
			
		||||
cobalt = "0.9.2"
 | 
			
		||||
commonsCli = "1.6.0"
 | 
			
		||||
jetbrainsAnnotations = "24.1.0"
 | 
			
		||||
jsr305 = "3.0.2"
 | 
			
		||||
@@ -51,10 +51,11 @@ sodium = "mc1.20-0.4.10"
 | 
			
		||||
hamcrest = "2.2"
 | 
			
		||||
jqwik = "1.8.2"
 | 
			
		||||
junit = "5.10.1"
 | 
			
		||||
jmh = "1.37"
 | 
			
		||||
 | 
			
		||||
# Build tools
 | 
			
		||||
cctJavadoc = "1.8.2"
 | 
			
		||||
checkstyle = "10.12.6"
 | 
			
		||||
checkstyle = "10.14.1"
 | 
			
		||||
curseForgeGradle = "1.1.18"
 | 
			
		||||
errorProne-core = "2.23.0"
 | 
			
		||||
errorProne-plugin = "3.1.0"
 | 
			
		||||
@@ -62,15 +63,15 @@ fabric-loom = "1.5.7"
 | 
			
		||||
githubRelease = "2.5.2"
 | 
			
		||||
gradleVersions = "0.50.0"
 | 
			
		||||
ideaExt = "1.1.7"
 | 
			
		||||
illuaminate = "0.1.0-44-g9ee0055"
 | 
			
		||||
illuaminate = "0.1.0-69-gf294ab2"
 | 
			
		||||
lwjgl = "3.3.3"
 | 
			
		||||
minotaur = "2.8.7"
 | 
			
		||||
neoGradle = "7.0.93"
 | 
			
		||||
neoGradle = "7.0.100"
 | 
			
		||||
nullAway = "0.9.9"
 | 
			
		||||
spotless = "6.23.3"
 | 
			
		||||
taskTree = "2.1.1"
 | 
			
		||||
teavm = "0.10.0-SQUID.2"
 | 
			
		||||
vanillaExtract = "0.1.1"
 | 
			
		||||
teavm = "0.10.0-SQUID.3"
 | 
			
		||||
vanillaExtract = "0.1.2"
 | 
			
		||||
versionCatalogUpdate = "0.8.1"
 | 
			
		||||
 | 
			
		||||
[libraries]
 | 
			
		||||
@@ -125,6 +126,8 @@ junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.re
 | 
			
		||||
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }
 | 
			
		||||
junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" }
 | 
			
		||||
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
 | 
			
		||||
jmh = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" }
 | 
			
		||||
jmh-processor = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" }
 | 
			
		||||
 | 
			
		||||
# LWJGL
 | 
			
		||||
lwjgl-bom = { module = "org.lwjgl:lwjgl-bom", version.ref = "lwjgl" }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
distributionBase=GRADLE_USER_HOME
 | 
			
		||||
distributionPath=wrapper/dists
 | 
			
		||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
 | 
			
		||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
 | 
			
		||||
networkTimeout=10000
 | 
			
		||||
validateDistributionUrl=true
 | 
			
		||||
zipStoreBase=GRADLE_USER_HOME
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								gradlew.bat
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								gradlew.bat
									
									
									
									
										vendored
									
									
								
							@@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
 | 
			
		||||
%JAVA_EXE% -version >NUL 2>&1
 | 
			
		||||
if %ERRORLEVEL% equ 0 goto execute
 | 
			
		||||
 | 
			
		||||
echo.
 | 
			
		||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
 | 
			
		||||
echo.
 | 
			
		||||
echo Please set the JAVA_HOME variable in your environment to match the
 | 
			
		||||
echo location of your Java installation.
 | 
			
		||||
echo. 1>&2
 | 
			
		||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
 | 
			
		||||
echo. 1>&2
 | 
			
		||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
 | 
			
		||||
echo location of your Java installation. 1>&2
 | 
			
		||||
 | 
			
		||||
goto fail
 | 
			
		||||
 | 
			
		||||
@@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
 | 
			
		||||
 | 
			
		||||
if exist "%JAVA_EXE%" goto execute
 | 
			
		||||
 | 
			
		||||
echo.
 | 
			
		||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
 | 
			
		||||
echo.
 | 
			
		||||
echo Please set the JAVA_HOME variable in your environment to match the
 | 
			
		||||
echo location of your Java installation.
 | 
			
		||||
echo. 1>&2
 | 
			
		||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
 | 
			
		||||
echo. 1>&2
 | 
			
		||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
 | 
			
		||||
echo location of your Java installation. 1>&2
 | 
			
		||||
 | 
			
		||||
goto fail
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
package dan200.computercraft.api.network.wired;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import org.jetbrains.annotations.ApiStatus;
 | 
			
		||||
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
@@ -22,6 +23,7 @@ import java.util.Map;
 | 
			
		||||
 *
 | 
			
		||||
 * @see WiredNode#getNetwork()
 | 
			
		||||
 */
 | 
			
		||||
@ApiStatus.NonExtendable
 | 
			
		||||
public interface WiredNetwork {
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a connection between two nodes.
 | 
			
		||||
@@ -35,7 +37,9 @@ public interface WiredNetwork {
 | 
			
		||||
     * @throws IllegalArgumentException If {@code left} and {@code right} are equal.
 | 
			
		||||
     * @see WiredNode#connectTo(WiredNode)
 | 
			
		||||
     * @see WiredNetwork#connect(WiredNode, WiredNode)
 | 
			
		||||
     * @deprecated Use {@link WiredNode#connectTo(WiredNode)}
 | 
			
		||||
     */
 | 
			
		||||
    @Deprecated
 | 
			
		||||
    boolean connect(WiredNode left, WiredNode right);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -50,7 +54,9 @@ public interface WiredNetwork {
 | 
			
		||||
     * @throws IllegalArgumentException If {@code left} and {@code right} are equal.
 | 
			
		||||
     * @see WiredNode#disconnectFrom(WiredNode)
 | 
			
		||||
     * @see WiredNetwork#connect(WiredNode, WiredNode)
 | 
			
		||||
     * @deprecated Use {@link WiredNode#disconnectFrom(WiredNode)}
 | 
			
		||||
     */
 | 
			
		||||
    @Deprecated
 | 
			
		||||
    boolean disconnect(WiredNode left, WiredNode right);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -64,7 +70,9 @@ public interface WiredNetwork {
 | 
			
		||||
     * only element.
 | 
			
		||||
     * @throws IllegalArgumentException If the node is not in the network.
 | 
			
		||||
     * @see WiredNode#remove()
 | 
			
		||||
     * @deprecated Use {@link WiredNode#remove()}
 | 
			
		||||
     */
 | 
			
		||||
    @Deprecated
 | 
			
		||||
    boolean remove(WiredNode node);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -77,6 +85,8 @@ public interface WiredNetwork {
 | 
			
		||||
     * @param peripherals The new peripherals for this node.
 | 
			
		||||
     * @throws IllegalArgumentException If the node is not in the network.
 | 
			
		||||
     * @see WiredNode#updatePeripherals(Map)
 | 
			
		||||
     * @deprecated Use {@link WiredNode#updatePeripherals(Map)}
 | 
			
		||||
     */
 | 
			
		||||
    @Deprecated
 | 
			
		||||
    void updatePeripherals(WiredNode node, Map<String, IPeripheral> peripherals);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ package dan200.computercraft.api.network.wired;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.network.PacketNetwork;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import org.jetbrains.annotations.ApiStatus;
 | 
			
		||||
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
@@ -22,6 +23,7 @@ import java.util.Map;
 | 
			
		||||
 * Wired nodes also provide several convenience methods for interacting with a wired network. These should only ever
 | 
			
		||||
 * be used on the main server thread.
 | 
			
		||||
 */
 | 
			
		||||
@ApiStatus.NonExtendable
 | 
			
		||||
public interface WiredNode extends PacketNetwork {
 | 
			
		||||
    /**
 | 
			
		||||
     * The associated element for this network node.
 | 
			
		||||
@@ -37,7 +39,9 @@ public interface WiredNode extends PacketNetwork {
 | 
			
		||||
     * This should only be used on the server thread.
 | 
			
		||||
     *
 | 
			
		||||
     * @return This node's network.
 | 
			
		||||
     * @deprecated Use the connect/disconnect/remove methods on {@link WiredNode}.
 | 
			
		||||
     */
 | 
			
		||||
    @Deprecated
 | 
			
		||||
    WiredNetwork getNetwork();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -47,12 +51,9 @@ public interface WiredNode extends PacketNetwork {
 | 
			
		||||
     *
 | 
			
		||||
     * @param node The other node to connect to.
 | 
			
		||||
     * @return {@code true} if a connection was created or {@code false} if the connection already exists.
 | 
			
		||||
     * @see WiredNetwork#connect(WiredNode, WiredNode)
 | 
			
		||||
     * @see WiredNode#disconnectFrom(WiredNode)
 | 
			
		||||
     */
 | 
			
		||||
    default boolean connectTo(WiredNode node) {
 | 
			
		||||
        return getNetwork().connect(this, node);
 | 
			
		||||
    }
 | 
			
		||||
    boolean connectTo(WiredNode node);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Destroy a connection between this node and another.
 | 
			
		||||
@@ -61,13 +62,9 @@ public interface WiredNode extends PacketNetwork {
 | 
			
		||||
     *
 | 
			
		||||
     * @param node The other node to disconnect from.
 | 
			
		||||
     * @return {@code true} if a connection was destroyed or {@code false} if no connection exists.
 | 
			
		||||
     * @throws IllegalArgumentException If {@code node} is not on the same network.
 | 
			
		||||
     * @see WiredNetwork#disconnect(WiredNode, WiredNode)
 | 
			
		||||
     * @see WiredNode#connectTo(WiredNode)
 | 
			
		||||
     */
 | 
			
		||||
    default boolean disconnectFrom(WiredNode node) {
 | 
			
		||||
        return getNetwork().disconnect(this, node);
 | 
			
		||||
    }
 | 
			
		||||
    boolean disconnectFrom(WiredNode node);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sever all connections this node has, removing it from this network.
 | 
			
		||||
@@ -78,11 +75,8 @@ public interface WiredNode extends PacketNetwork {
 | 
			
		||||
     * @return Whether this node was removed from the network. One cannot remove a node from a network where it is the
 | 
			
		||||
     * only element.
 | 
			
		||||
     * @throws IllegalArgumentException If the node is not in the network.
 | 
			
		||||
     * @see WiredNetwork#remove(WiredNode)
 | 
			
		||||
     */
 | 
			
		||||
    default boolean remove() {
 | 
			
		||||
        return getNetwork().remove(this);
 | 
			
		||||
    }
 | 
			
		||||
    boolean remove();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Mark this node's peripherals as having changed.
 | 
			
		||||
@@ -91,9 +85,6 @@ public interface WiredNode extends PacketNetwork {
 | 
			
		||||
     * that your network element owns.
 | 
			
		||||
     *
 | 
			
		||||
     * @param peripherals The new peripherals for this node.
 | 
			
		||||
     * @see WiredNetwork#updatePeripherals(WiredNode, Map)
 | 
			
		||||
     */
 | 
			
		||||
    default void updatePeripherals(Map<String, IPeripheral> peripherals) {
 | 
			
		||||
        getNetwork().updatePeripherals(this, peripherals);
 | 
			
		||||
    }
 | 
			
		||||
    void updatePeripherals(Map<String, IPeripheral> peripherals);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ public final class Services {
 | 
			
		||||
     * @throws IllegalStateException When the service cannot be loaded.
 | 
			
		||||
     */
 | 
			
		||||
    public static <T> T load(Class<T> klass) {
 | 
			
		||||
        var services = ServiceLoader.load(klass).stream().toList();
 | 
			
		||||
        var services = ServiceLoader.load(klass, klass.getClassLoader()).stream().toList();
 | 
			
		||||
        return switch (services.size()) {
 | 
			
		||||
            case 1 -> services.get(0).get();
 | 
			
		||||
            case 0 -> throw new IllegalStateException("Cannot find service for " + klass.getName());
 | 
			
		||||
 
 | 
			
		||||
@@ -46,6 +46,9 @@ dependencies {
 | 
			
		||||
    testImplementation(libs.bundles.test)
 | 
			
		||||
    testRuntimeOnly(libs.bundles.testRuntime)
 | 
			
		||||
 | 
			
		||||
    testImplementation(libs.jmh)
 | 
			
		||||
    testAnnotationProcessor(libs.jmh.processor)
 | 
			
		||||
 | 
			
		||||
    testModCompileOnly(libs.mixin)
 | 
			
		||||
    testModImplementation(testFixtures(project(":core")))
 | 
			
		||||
    testModImplementation(testFixtures(project(":common")))
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@ import dan200.computercraft.core.util.Colour;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import dan200.computercraft.shared.command.CommandComputerCraft;
 | 
			
		||||
import dan200.computercraft.shared.common.IColouredItem;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerState;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ServerContext;
 | 
			
		||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
 | 
			
		||||
import dan200.computercraft.shared.computer.inventory.ViewComputerMenu;
 | 
			
		||||
@@ -81,13 +82,18 @@ public final class ClientRegistry {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Register any client-side objects which must be done on the main thread.
 | 
			
		||||
     *
 | 
			
		||||
     * @param itemProperties Callback to register item properties.
 | 
			
		||||
     */
 | 
			
		||||
    public static void registerMainThread() {
 | 
			
		||||
        registerItemProperty("state",
 | 
			
		||||
            new UnclampedPropertyFunction((stack, world, player, random) -> ClientPocketComputers.get(stack).getState().ordinal()),
 | 
			
		||||
    public static void registerMainThread(RegisterItemProperty itemProperties) {
 | 
			
		||||
        registerItemProperty(itemProperties, "state",
 | 
			
		||||
            new UnclampedPropertyFunction((stack, world, player, random) -> {
 | 
			
		||||
                var computer = ClientPocketComputers.get(stack);
 | 
			
		||||
                return (computer == null ? ComputerState.OFF : computer.getState()).ordinal();
 | 
			
		||||
            }),
 | 
			
		||||
            ModRegistry.Items.POCKET_COMPUTER_NORMAL, ModRegistry.Items.POCKET_COMPUTER_ADVANCED
 | 
			
		||||
        );
 | 
			
		||||
        registerItemProperty("coloured",
 | 
			
		||||
        registerItemProperty(itemProperties, "coloured",
 | 
			
		||||
            (stack, world, player, random) -> IColouredItem.getColourBasic(stack) != -1 ? 1 : 0,
 | 
			
		||||
            ModRegistry.Items.POCKET_COMPUTER_NORMAL, ModRegistry.Items.POCKET_COMPUTER_ADVANCED
 | 
			
		||||
        );
 | 
			
		||||
@@ -125,9 +131,17 @@ public final class ClientRegistry {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @SafeVarargs
 | 
			
		||||
    private static void registerItemProperty(String name, ClampedItemPropertyFunction getter, Supplier<? extends Item>... items) {
 | 
			
		||||
    private static void registerItemProperty(RegisterItemProperty itemProperties, String name, ClampedItemPropertyFunction getter, Supplier<? extends Item>... items) {
 | 
			
		||||
        var id = new ResourceLocation(ComputerCraftAPI.MOD_ID, name);
 | 
			
		||||
        for (var item : items) ItemProperties.register(item.get(), id, getter);
 | 
			
		||||
        for (var item : items) itemProperties.register(item.get(), id, getter);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Register an item property via {@link ItemProperties#register}. Forge and Fabric expose different methods, so we
 | 
			
		||||
     * supply this via mod-loader-specific code.
 | 
			
		||||
     */
 | 
			
		||||
    public interface RegisterItemProperty {
 | 
			
		||||
        void register(Item item, ResourceLocation name, ClampedItemPropertyFunction property);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void registerReloadListeners(Consumer<PreparableReloadListener> register, Minecraft minecraft) {
 | 
			
		||||
@@ -165,17 +179,14 @@ public final class ClientRegistry {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static int getPocketColour(ItemStack stack, int layer) {
 | 
			
		||||
        switch (layer) {
 | 
			
		||||
            case 0:
 | 
			
		||||
            default:
 | 
			
		||||
                return 0xFFFFFF;
 | 
			
		||||
            case 1: // Frame colour
 | 
			
		||||
                return IColouredItem.getColourBasic(stack);
 | 
			
		||||
            case 2: { // Light colour
 | 
			
		||||
                var light = ClientPocketComputers.get(stack).getLightState();
 | 
			
		||||
                return light == -1 ? Colour.BLACK.getHex() : light;
 | 
			
		||||
            }
 | 
			
		||||
        return switch (layer) {
 | 
			
		||||
            default -> 0xFFFFFF;
 | 
			
		||||
            case 1 -> IColouredItem.getColourBasic(stack); // Frame colour
 | 
			
		||||
            case 2 -> { // Light colour
 | 
			
		||||
                var computer = ClientPocketComputers.get(stack);
 | 
			
		||||
                yield computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static int getTurtleColour(ItemStack stack, int layer) {
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ import dan200.computercraft.shared.computer.terminal.TerminalState;
 | 
			
		||||
import dan200.computercraft.shared.computer.upload.UploadResult;
 | 
			
		||||
import dan200.computercraft.shared.network.client.ClientNetworkContext;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
@@ -27,7 +28,6 @@ import net.minecraft.world.entity.player.Player;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -67,19 +67,17 @@ public final class ClientNetworkContextImpl implements ClientNetworkContext {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void handlePocketComputerData(int instanceId, ComputerState state, int lightState, TerminalState terminal) {
 | 
			
		||||
        var computer = ClientPocketComputers.get(instanceId, terminal.colour);
 | 
			
		||||
        computer.setState(state, lightState);
 | 
			
		||||
        if (terminal.hasTerminal()) computer.setTerminal(terminal);
 | 
			
		||||
    public void handlePocketComputerData(UUID instanceId, ComputerState state, int lightState, TerminalState terminal) {
 | 
			
		||||
        ClientPocketComputers.setState(instanceId, state, lightState, terminal);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void handlePocketComputerDeleted(int instanceId) {
 | 
			
		||||
    public void handlePocketComputerDeleted(UUID instanceId) {
 | 
			
		||||
        ClientPocketComputers.remove(instanceId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, ByteBuffer buffer) {
 | 
			
		||||
    public void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, EncodedAudio buffer) {
 | 
			
		||||
        SpeakerManager.getSound(source).playAudio(reifyPosition(position), volume, buffer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,21 +4,26 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.pocket;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerState;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ServerComputer;
 | 
			
		||||
import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
 | 
			
		||||
import dan200.computercraft.shared.computer.terminal.TerminalState;
 | 
			
		||||
import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
 | 
			
		||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
 | 
			
		||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 | 
			
		||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Maps {@link ServerComputer#getInstanceID()} to locals {@link PocketComputerData}.
 | 
			
		||||
 * Maps {@link ServerComputer#getInstanceUUID()} to locals {@link PocketComputerData}.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This is populated by {@link PocketComputerDataMessage} and accessed when rendering pocket computers
 | 
			
		||||
 */
 | 
			
		||||
public final class ClientPocketComputers {
 | 
			
		||||
    private static final Int2ObjectMap<PocketComputerData> instances = new Int2ObjectOpenHashMap<>();
 | 
			
		||||
    private static final Map<UUID, PocketComputerData> instances = new HashMap<>();
 | 
			
		||||
 | 
			
		||||
    private ClientPocketComputers() {
 | 
			
		||||
    }
 | 
			
		||||
@@ -27,25 +32,32 @@ public final class ClientPocketComputers {
 | 
			
		||||
        instances.clear();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void remove(int id) {
 | 
			
		||||
    public static void remove(UUID id) {
 | 
			
		||||
        instances.remove(id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get or create a pocket computer.
 | 
			
		||||
     * Set the state of a pocket computer.
 | 
			
		||||
     *
 | 
			
		||||
     * @param instanceId   The instance ID of the pocket computer.
 | 
			
		||||
     * @param advanced   Whether this computer has an advanced terminal.
 | 
			
		||||
     * @return The pocket computer data.
 | 
			
		||||
     * @param state        The computer state of the pocket computer.
 | 
			
		||||
     * @param lightColour  The current colour of the modem light.
 | 
			
		||||
     * @param terminalData The current terminal contents.
 | 
			
		||||
     */
 | 
			
		||||
    public static PocketComputerData get(int instanceId, boolean advanced) {
 | 
			
		||||
    public static void setState(UUID instanceId, ComputerState state, int lightColour, TerminalState terminalData) {
 | 
			
		||||
        var computer = instances.get(instanceId);
 | 
			
		||||
        if (computer == null) instances.put(instanceId, computer = new PocketComputerData(advanced));
 | 
			
		||||
        return computer;
 | 
			
		||||
        if (computer == null) {
 | 
			
		||||
            var terminal = new NetworkedTerminal(terminalData.width, terminalData.height, terminalData.colour);
 | 
			
		||||
            instances.put(instanceId, computer = new PocketComputerData(state, lightColour, terminal));
 | 
			
		||||
        } else {
 | 
			
		||||
            computer.setState(state, lightColour);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    public static PocketComputerData get(ItemStack stack) {
 | 
			
		||||
        var family = stack.getItem() instanceof PocketComputerItem computer ? computer.getFamily() : ComputerFamily.NORMAL;
 | 
			
		||||
        return get(PocketComputerItem.getInstanceID(stack), family != ComputerFamily.NORMAL);
 | 
			
		||||
        if (terminalData.hasTerminal()) terminalData.apply(computer.getTerminal());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static @Nullable PocketComputerData get(ItemStack stack) {
 | 
			
		||||
        var id = PocketComputerItem.getInstanceID(stack);
 | 
			
		||||
        return id == null ? null : instances.get(id);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,11 +4,8 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.pocket;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.core.terminal.Terminal;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerState;
 | 
			
		||||
import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
 | 
			
		||||
import dan200.computercraft.shared.computer.terminal.TerminalState;
 | 
			
		||||
import dan200.computercraft.shared.config.Config;
 | 
			
		||||
import dan200.computercraft.shared.pocket.core.PocketServerComputer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -21,20 +18,22 @@ import dan200.computercraft.shared.pocket.core.PocketServerComputer;
 | 
			
		||||
 * @see ClientPocketComputers The registry which holds pocket computers.
 | 
			
		||||
 * @see PocketServerComputer The server-side pocket computer.
 | 
			
		||||
 */
 | 
			
		||||
public class PocketComputerData {
 | 
			
		||||
public final class PocketComputerData {
 | 
			
		||||
    private final NetworkedTerminal terminal;
 | 
			
		||||
    private ComputerState state = ComputerState.OFF;
 | 
			
		||||
    private int lightColour = -1;
 | 
			
		||||
    private ComputerState state;
 | 
			
		||||
    private int lightColour;
 | 
			
		||||
 | 
			
		||||
    public PocketComputerData(boolean colour) {
 | 
			
		||||
        terminal = new NetworkedTerminal(Config.pocketTermWidth, Config.pocketTermHeight, colour);
 | 
			
		||||
    PocketComputerData(ComputerState state, int lightColour, NetworkedTerminal terminal) {
 | 
			
		||||
        this.state = state;
 | 
			
		||||
        this.lightColour = lightColour;
 | 
			
		||||
        this.terminal = terminal;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public int getLightState() {
 | 
			
		||||
        return state != ComputerState.OFF ? lightColour : -1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Terminal getTerminal() {
 | 
			
		||||
    public NetworkedTerminal getTerminal() {
 | 
			
		||||
        return terminal;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -42,12 +41,8 @@ public class PocketComputerData {
 | 
			
		||||
        return state;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setState(ComputerState state, int lightColour) {
 | 
			
		||||
    void setState(ComputerState state, int lightColour) {
 | 
			
		||||
        this.state = state;
 | 
			
		||||
        this.lightColour = lightColour;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setTerminal(TerminalState state) {
 | 
			
		||||
        state.apply(terminal);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ import dan200.computercraft.client.pocket.ClientPocketComputers;
 | 
			
		||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
 | 
			
		||||
import dan200.computercraft.core.util.Colour;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import dan200.computercraft.shared.config.Config;
 | 
			
		||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
 | 
			
		||||
import net.minecraft.client.renderer.MultiBufferSource;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
@@ -32,10 +33,16 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void renderItem(PoseStack transform, MultiBufferSource bufferSource, ItemStack stack, int light) {
 | 
			
		||||
        var computer = ClientPocketComputers.get(stack);
 | 
			
		||||
        var terminal = computer.getTerminal();
 | 
			
		||||
        var terminal = computer == null ? null : computer.getTerminal();
 | 
			
		||||
 | 
			
		||||
        var termWidth = terminal.getWidth();
 | 
			
		||||
        var termHeight = terminal.getHeight();
 | 
			
		||||
        int termWidth, termHeight;
 | 
			
		||||
        if (terminal == null) {
 | 
			
		||||
            termWidth = Config.pocketTermWidth;
 | 
			
		||||
            termHeight = Config.pocketTermHeight;
 | 
			
		||||
        } else {
 | 
			
		||||
            termWidth = terminal.getWidth();
 | 
			
		||||
            termHeight = terminal.getHeight();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var width = termWidth * FONT_WIDTH + MARGIN * 2;
 | 
			
		||||
        var height = termHeight * FONT_HEIGHT + MARGIN * 2;
 | 
			
		||||
@@ -60,14 +67,15 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
 | 
			
		||||
        renderFrame(matrix, bufferSource, family, frameColour, light, width, height);
 | 
			
		||||
 | 
			
		||||
        // Render the light
 | 
			
		||||
        var lightColour = ClientPocketComputers.get(stack).getLightState();
 | 
			
		||||
        if (lightColour == -1) lightColour = Colour.BLACK.getHex();
 | 
			
		||||
        var lightColour = computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState();
 | 
			
		||||
        renderLight(transform, bufferSource, lightColour, width, height);
 | 
			
		||||
 | 
			
		||||
        FixedWidthFontRenderer.drawTerminal(
 | 
			
		||||
            FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL)),
 | 
			
		||||
            MARGIN, MARGIN, terminal, MARGIN, MARGIN, MARGIN, MARGIN
 | 
			
		||||
        );
 | 
			
		||||
        var quadEmitter = FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL));
 | 
			
		||||
        if (terminal == null) {
 | 
			
		||||
            FixedWidthFontRenderer.drawEmptyTerminal(quadEmitter, 0, 0, width, height);
 | 
			
		||||
        } else {
 | 
			
		||||
            FixedWidthFontRenderer.drawTerminal(quadEmitter, MARGIN, MARGIN, terminal, MARGIN, MARGIN, MARGIN, MARGIN);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        transform.popPose();
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -60,9 +60,9 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
 | 
			
		||||
    @Override
 | 
			
		||||
    public void render(MonitorBlockEntity monitor, float partialTicks, PoseStack transform, MultiBufferSource bufferSource, int lightmapCoord, int overlayLight) {
 | 
			
		||||
        // Render from the origin monitor
 | 
			
		||||
        var originTerminal = monitor.getClientMonitor();
 | 
			
		||||
 | 
			
		||||
        var originTerminal = monitor.getOriginClientMonitor();
 | 
			
		||||
        if (originTerminal == null) return;
 | 
			
		||||
 | 
			
		||||
        var origin = originTerminal.getOrigin();
 | 
			
		||||
        var renderState = originTerminal.getRenderState(MonitorRenderState::new);
 | 
			
		||||
        var monitorPos = monitor.getBlockPos();
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
package dan200.computercraft.client.sound;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.audio.Channel;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
 | 
			
		||||
import net.minecraft.client.sounds.AudioStream;
 | 
			
		||||
@@ -36,7 +37,7 @@ class DfpwmStream implements AudioStream {
 | 
			
		||||
    /**
 | 
			
		||||
     * The {@link Channel} which this sound is playing on.
 | 
			
		||||
     *
 | 
			
		||||
     * @see SpeakerInstance#playAudio(SpeakerPosition, float, ByteBuffer)
 | 
			
		||||
     * @see SpeakerInstance#playAudio(SpeakerPosition, float, EncodedAudio)
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    Channel channel;
 | 
			
		||||
@@ -44,21 +45,23 @@ class DfpwmStream implements AudioStream {
 | 
			
		||||
    /**
 | 
			
		||||
     * The underlying {@link SoundEngine} executor.
 | 
			
		||||
     *
 | 
			
		||||
     * @see SpeakerInstance#playAudio(SpeakerPosition, float, ByteBuffer)
 | 
			
		||||
     * @see SpeakerInstance#playAudio(SpeakerPosition, float, EncodedAudio)
 | 
			
		||||
     * @see SoundEngine#executor
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    Executor executor;
 | 
			
		||||
 | 
			
		||||
    private int charge = 0; // q
 | 
			
		||||
    private int strength = 0; // s
 | 
			
		||||
    private int lowPassCharge;
 | 
			
		||||
    private boolean previousBit = false;
 | 
			
		||||
 | 
			
		||||
    DfpwmStream() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void push(ByteBuffer input) {
 | 
			
		||||
    void push(EncodedAudio audio) {
 | 
			
		||||
        var charge = audio.charge();
 | 
			
		||||
        var strength = audio.strength();
 | 
			
		||||
        var previousBit = audio.previousBit();
 | 
			
		||||
        var input = audio.audio();
 | 
			
		||||
 | 
			
		||||
        var readable = input.remaining();
 | 
			
		||||
        var output = ByteBuffer.allocate(readable * 8).order(ByteOrder.nativeOrder());
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,12 +6,12 @@ package dan200.computercraft.client.sound;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.core.util.Nullability;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An instance of a speaker, which is either playing a {@link DfpwmStream} stream or a normal sound.
 | 
			
		||||
@@ -25,7 +25,7 @@ public class SpeakerInstance {
 | 
			
		||||
    SpeakerInstance() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void pushAudio(ByteBuffer buffer) {
 | 
			
		||||
    private void pushAudio(EncodedAudio buffer) {
 | 
			
		||||
        var sound = this.sound;
 | 
			
		||||
 | 
			
		||||
        var stream = currentStream;
 | 
			
		||||
@@ -43,7 +43,7 @@ public class SpeakerInstance {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void playAudio(SpeakerPosition position, float volume, ByteBuffer buffer) {
 | 
			
		||||
    public void playAudio(SpeakerPosition position, float volume, EncodedAudio buffer) {
 | 
			
		||||
        pushAudio(buffer);
 | 
			
		||||
 | 
			
		||||
        var soundManager = Minecraft.getInstance().getSoundManager();
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ import dan200.computercraft.api.upgrades.UpgradeBase;
 | 
			
		||||
import dan200.computercraft.core.metrics.Metric;
 | 
			
		||||
import dan200.computercraft.core.metrics.Metrics;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import dan200.computercraft.shared.command.arguments.ComputerSelector;
 | 
			
		||||
import dan200.computercraft.shared.computer.metrics.basic.Aggregate;
 | 
			
		||||
import dan200.computercraft.shared.computer.metrics.basic.AggregatedMetric;
 | 
			
		||||
import dan200.computercraft.shared.config.ConfigFile;
 | 
			
		||||
@@ -165,10 +166,19 @@ public final class LanguageProvider implements DataProvider {
 | 
			
		||||
        add("commands.computercraft.generic.exception", "Unhandled exception (%s)");
 | 
			
		||||
        add("commands.computercraft.generic.additional_rows", "%d additional rows…");
 | 
			
		||||
 | 
			
		||||
        // Argument types
 | 
			
		||||
        add("argument.computercraft.computer.instance", "Unique instance ID");
 | 
			
		||||
        add("argument.computercraft.computer.id", "Computer ID");
 | 
			
		||||
        add("argument.computercraft.computer.label", "Computer label");
 | 
			
		||||
        add("argument.computercraft.computer.distance", "Distance to entity");
 | 
			
		||||
        add("argument.computercraft.computer.family", "Computer family");
 | 
			
		||||
 | 
			
		||||
        // Exceptions
 | 
			
		||||
        add("argument.computercraft.computer.no_matching", "No computers matching '%s'");
 | 
			
		||||
        add("argument.computercraft.computer.many_matching", "Multiple computers matching '%s' (instances %s)");
 | 
			
		||||
        add("argument.computercraft.tracking_field.no_field", "Unknown field '%s'");
 | 
			
		||||
        add("argument.computercraft.argument_expected", "Argument expected");
 | 
			
		||||
        add("argument.computercraft.unknown_computer_family", "Unknown computer family '%s'");
 | 
			
		||||
 | 
			
		||||
        // Metrics
 | 
			
		||||
        add(Metrics.COMPUTER_TASKS, "Tasks");
 | 
			
		||||
@@ -281,7 +291,8 @@ public final class LanguageProvider implements DataProvider {
 | 
			
		||||
            pocketUpgrades.getGeneratedUpgrades().stream().map(UpgradeBase::getUnlocalisedAdjective),
 | 
			
		||||
            Metric.metrics().values().stream().map(x -> AggregatedMetric.TRANSLATION_PREFIX + x.name() + ".name"),
 | 
			
		||||
            ConfigSpec.serverSpec.entries().map(ConfigFile.Entry::translationKey),
 | 
			
		||||
            ConfigSpec.clientSpec.entries().map(ConfigFile.Entry::translationKey)
 | 
			
		||||
            ConfigSpec.clientSpec.entries().map(ConfigFile.Entry::translationKey),
 | 
			
		||||
            ComputerSelector.options().values().stream().map(ComputerSelector.Option::translationKey)
 | 
			
		||||
        ).flatMap(x -> x);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,45 +4,66 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.impl.network.wired;
 | 
			
		||||
 | 
			
		||||
import org.jetbrains.annotations.Contract;
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Verifies certain elements of a network are "well formed".
 | 
			
		||||
 * Verifies certain elements of a network are well-formed.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This adds substantial overhead to network modification, and so should only be enabled
 | 
			
		||||
 * in a development environment.
 | 
			
		||||
 * This adds substantial overhead to network modification, and so is only enabled when assertions are enabled.
 | 
			
		||||
 */
 | 
			
		||||
public final class InvariantChecker {
 | 
			
		||||
final class InvariantChecker {
 | 
			
		||||
    private static final Logger LOG = LoggerFactory.getLogger(InvariantChecker.class);
 | 
			
		||||
    private static final boolean ENABLED = false;
 | 
			
		||||
 | 
			
		||||
    private InvariantChecker() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void checkNode(WiredNodeImpl node) {
 | 
			
		||||
        if (!ENABLED) return;
 | 
			
		||||
 | 
			
		||||
        var network = node.network;
 | 
			
		||||
        if (network == null) {
 | 
			
		||||
            LOG.error("Node's network is null", new Exception());
 | 
			
		||||
            return;
 | 
			
		||||
    static void checkNode(WiredNodeImpl node) {
 | 
			
		||||
        assert checkNodeImpl(node) : "Node invariants failed. See logs.";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        if (network.nodes == null || !network.nodes.contains(node)) {
 | 
			
		||||
            LOG.error("Node's network does not contain node", new Exception());
 | 
			
		||||
    private static boolean checkNodeImpl(WiredNodeImpl node) {
 | 
			
		||||
        var okay = true;
 | 
			
		||||
 | 
			
		||||
        if (node.currentSet != null) {
 | 
			
		||||
            okay = false;
 | 
			
		||||
            LOG.error("{}: currentSet was not cleared.", node);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var network = makeNullable(node.network);
 | 
			
		||||
        if (network == null) {
 | 
			
		||||
            okay = false;
 | 
			
		||||
            LOG.error("{}: Node's network is null.", node);
 | 
			
		||||
        } else if (makeNullable(network.nodes) == null || !network.nodes.contains(node)) {
 | 
			
		||||
            okay = false;
 | 
			
		||||
            LOG.error("{}: Node's network does not contain node.", node);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (var neighbour : node.neighbours) {
 | 
			
		||||
            if (!neighbour.neighbours.contains(node)) {
 | 
			
		||||
                LOG.error("Neighbour is missing node", new Exception());
 | 
			
		||||
            }
 | 
			
		||||
                okay = false;
 | 
			
		||||
                LOG.error("{}: Neighbour {}'s neighbour set does not contain origianl node.", node, neighbour);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    public static void checkNetwork(WiredNetworkImpl network) {
 | 
			
		||||
        if (!ENABLED) return;
 | 
			
		||||
        return okay;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        for (var node : network.nodes) checkNode(node);
 | 
			
		||||
    static void checkNetwork(WiredNetworkImpl network) {
 | 
			
		||||
        assert checkNetworkImpl(network) : "Network invariants failed. See logs.";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static boolean checkNetworkImpl(WiredNetworkImpl network) {
 | 
			
		||||
        var okay = true;
 | 
			
		||||
        for (var node : network.nodes) okay &= checkNodeImpl(node);
 | 
			
		||||
        return okay;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Contract("")
 | 
			
		||||
    private static <T> @Nullable T makeNullable(T object) {
 | 
			
		||||
        return object;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,100 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.impl.network.wired;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.network.wired.WiredNode;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A disjoint-set/union-find of {@link WiredNodeImpl}s.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Rather than actually maintaining a list of included nodes, wired nodes store {@linkplain WiredNodeImpl#currentSet the
 | 
			
		||||
 * set they're part of}. This means that we can only have one disjoint-set at once, but that is not a problem in
 | 
			
		||||
 * practice.
 | 
			
		||||
 *
 | 
			
		||||
 * @see WiredNodeImpl#currentSet
 | 
			
		||||
 * @see WiredNetworkImpl#remove(WiredNode)
 | 
			
		||||
 * @see <a href="https://en.wikipedia.org/wiki/Disjoint-set_data_structure">Disjoint-set data structure</a>
 | 
			
		||||
 */
 | 
			
		||||
class NodeSet {
 | 
			
		||||
    private NodeSet parent = this;
 | 
			
		||||
    private int size = 1;
 | 
			
		||||
    private @Nullable WiredNetworkImpl network;
 | 
			
		||||
 | 
			
		||||
    private boolean isRoot() {
 | 
			
		||||
        return parent == this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Resolve this union, finding the root {@link NodeSet}.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The root union.
 | 
			
		||||
     */
 | 
			
		||||
    NodeSet find() {
 | 
			
		||||
        var self = this;
 | 
			
		||||
        while (!self.isRoot()) self = self.parent = self.parent.parent;
 | 
			
		||||
        return self;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the size of this node set.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The size of the set.
 | 
			
		||||
     */
 | 
			
		||||
    int size() {
 | 
			
		||||
        return find().size;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add a node to this {@link NodeSet}.
 | 
			
		||||
     *
 | 
			
		||||
     * @param node The node to add to the set.
 | 
			
		||||
     */
 | 
			
		||||
    void addNode(WiredNodeImpl node) {
 | 
			
		||||
        if (!isRoot()) throw new IllegalStateException("Cannot grow a non-root set.");
 | 
			
		||||
        if (node.currentSet != null) throw new IllegalArgumentException("Node is already in a set.");
 | 
			
		||||
 | 
			
		||||
        node.currentSet = this;
 | 
			
		||||
        size++;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Merge two nodes sets together.
 | 
			
		||||
     *
 | 
			
		||||
     * @param left  The first union.
 | 
			
		||||
     * @param right The second union.
 | 
			
		||||
     * @return The union which was subsumed.
 | 
			
		||||
     */
 | 
			
		||||
    public static NodeSet merge(NodeSet left, NodeSet right) {
 | 
			
		||||
        if (!left.isRoot() || !right.isRoot()) throw new IllegalArgumentException("Cannot union a non-root set.");
 | 
			
		||||
        if (left == right) throw new IllegalArgumentException("Cannot merge a node into itself.");
 | 
			
		||||
 | 
			
		||||
        return left.size >= right.size ? mergeInto(left, right) : mergeInto(right, left);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static NodeSet mergeInto(NodeSet root, NodeSet child) {
 | 
			
		||||
        assert root.size > child.size;
 | 
			
		||||
        child.parent = root;
 | 
			
		||||
        root.size += child.size;
 | 
			
		||||
        return child;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void setNetwork(WiredNetworkImpl network) {
 | 
			
		||||
        if (!isRoot()) throw new IllegalStateException("Set is not the root.");
 | 
			
		||||
        if (this.network != null) throw new IllegalStateException("Set already has a network.");
 | 
			
		||||
        this.network = network;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the associated network.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The associated network.
 | 
			
		||||
     */
 | 
			
		||||
    WiredNetworkImpl network() {
 | 
			
		||||
        return Objects.requireNonNull(find().network);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,6 +6,7 @@ package dan200.computercraft.impl.network.wired;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.network.wired.WiredNetworkChange;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import dan200.computercraft.core.util.PeripheralHelpers;
 | 
			
		||||
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
@@ -52,7 +53,7 @@ final class WiredNetworkChangeImpl implements WiredNetworkChange {
 | 
			
		||||
            var oldValue = entry.getValue();
 | 
			
		||||
            if (newPeripherals.containsKey(oldKey)) {
 | 
			
		||||
                var rightValue = added.get(oldKey);
 | 
			
		||||
                if (oldValue.equals(rightValue)) {
 | 
			
		||||
                if (PeripheralHelpers.equals(oldValue, rightValue)) {
 | 
			
		||||
                    added.remove(oldKey);
 | 
			
		||||
                } else {
 | 
			
		||||
                    removed.put(oldKey, oldValue);
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import dan200.computercraft.api.network.Packet;
 | 
			
		||||
import dan200.computercraft.api.network.wired.WiredNetwork;
 | 
			
		||||
import dan200.computercraft.api.network.wired.WiredNode;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import dan200.computercraft.core.util.Nullability;
 | 
			
		||||
 | 
			
		||||
import java.util.*;
 | 
			
		||||
import java.util.concurrent.locks.ReadWriteLock;
 | 
			
		||||
@@ -187,10 +188,76 @@ final class WiredNetworkImpl implements WiredNetwork {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var reachable = reachableNodes(neighbours.iterator().next());
 | 
			
		||||
            assert neighbours.size() >= 2 : "Must have more than one neighbour.";
 | 
			
		||||
 | 
			
		||||
            /*
 | 
			
		||||
             Otherwise we need to find all sets of connected nodes within the graph, and split them off into their own
 | 
			
		||||
             networks.
 | 
			
		||||
 | 
			
		||||
             With our current graph representation[^1], this requires a traversal of the graph, taking O(|V| + |E))
 | 
			
		||||
             time, which can get quite expensive for large graphs. We try to avoid this traversal where possible, by
 | 
			
		||||
             optimising for the case where the graph remains fully connected after removing this node, for instance,
 | 
			
		||||
             removing "A" here:
 | 
			
		||||
 | 
			
		||||
               A---B            B
 | 
			
		||||
               |   |    =>      |
 | 
			
		||||
               C---D        C---D
 | 
			
		||||
 | 
			
		||||
             We observe that these sorts of loops tend to be local, and so try to identify them as quickly as possible.
 | 
			
		||||
             To do this, we do a standard breadth-first traversal of the graph starting at the neighbours of the
 | 
			
		||||
             removed node, building sets of connected nodes.
 | 
			
		||||
 | 
			
		||||
             If, at any point, all nodes visited so far are connected to each other, then we know all remaining nodes
 | 
			
		||||
             will also be connected. This allows us to abort our traversal of the graph, and just remove the node (much
 | 
			
		||||
             like we do in the single neighbour case above).
 | 
			
		||||
 | 
			
		||||
             Otherwise, we then just create a new network for each disjoint set of connected nodes.
 | 
			
		||||
 | 
			
		||||
             {^1]:
 | 
			
		||||
               There are efficient (near-logarithmic) algorithms for this (e.g. https://arxiv.org/pdf/1609.05867.pdf),
 | 
			
		||||
               but they are significantly more complex to implement.
 | 
			
		||||
            */
 | 
			
		||||
 | 
			
		||||
            // Create a new set of nodes for each neighbour, and add them to our queue of nodes to visit.
 | 
			
		||||
            List<WiredNodeImpl> queue = new ArrayList<>();
 | 
			
		||||
            Set<NodeSet> nodeSets = new HashSet<>(neighbours.size());
 | 
			
		||||
            for (var neighbour : neighbours) {
 | 
			
		||||
                nodeSets.add(neighbour.currentSet = new NodeSet());
 | 
			
		||||
                queue.add(neighbour);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Perform a breadth-first search of the graph, starting from the neighbours.
 | 
			
		||||
            graphSearch:
 | 
			
		||||
            for (var i = 0; i < queue.size(); i++) {
 | 
			
		||||
                var enqueuedNode = queue.get(i);
 | 
			
		||||
                for (var neighbour : enqueuedNode.neighbours) {
 | 
			
		||||
                    var nodeSet = Nullability.assertNonNull(enqueuedNode.currentSet).find();
 | 
			
		||||
 | 
			
		||||
                    // The neighbour has no set and so has not been visited yet. Add it to the current set and enqueue
 | 
			
		||||
                    // it to be visited.
 | 
			
		||||
                    if (neighbour.currentSet == null) {
 | 
			
		||||
                        nodeSet.addNode(neighbour);
 | 
			
		||||
                        queue.add(neighbour);
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Otherwise, take the union of the two nodes' sets if needed. If we've only got a single node set
 | 
			
		||||
                    // left, then we know the whole graph is network is connected (even if not all nodes have been
 | 
			
		||||
                    // visited) and so can abort early.
 | 
			
		||||
                    var neighbourSet = neighbour.currentSet.find();
 | 
			
		||||
                    if (nodeSet != neighbourSet) {
 | 
			
		||||
                        var removed = nodeSets.remove(NodeSet.merge(nodeSet, neighbourSet));
 | 
			
		||||
                        assert removed : "Merged set should have been ";
 | 
			
		||||
                        if (nodeSets.size() == 1) break graphSearch;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // If we have a single subset, then all nodes are reachable - just clear the set and exit.
 | 
			
		||||
            if (nodeSets.size() == 1) {
 | 
			
		||||
                assert nodeSets.iterator().next().size() == queue.size();
 | 
			
		||||
                for (var neighbour : queue) neighbour.currentSet = null;
 | 
			
		||||
 | 
			
		||||
            // If all nodes are reachable then exit.
 | 
			
		||||
            if (reachable.size() == nodes.size()) {
 | 
			
		||||
                // Broadcast our simple peripheral changes
 | 
			
		||||
                removeSingleNode(wired, wiredNetwork);
 | 
			
		||||
                InvariantChecker.checkNode(wired);
 | 
			
		||||
@@ -198,43 +265,46 @@ final class WiredNetworkImpl implements WiredNetwork {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // A split may cause 2..neighbours.size() separate networks, so we
 | 
			
		||||
            // iterate through our neighbour list, generating child networks.
 | 
			
		||||
            neighbours.removeAll(reachable);
 | 
			
		||||
            var maximals = new ArrayList<WiredNetworkImpl>(neighbours.size() + 1);
 | 
			
		||||
            maximals.add(wiredNetwork);
 | 
			
		||||
            maximals.add(new WiredNetworkImpl(reachable));
 | 
			
		||||
            assert queue.size() == nodes.size() : "Expected queue to contain all nodes.";
 | 
			
		||||
 | 
			
		||||
            while (!neighbours.isEmpty()) {
 | 
			
		||||
                reachable = reachableNodes(neighbours.iterator().next());
 | 
			
		||||
                neighbours.removeAll(reachable);
 | 
			
		||||
                maximals.add(new WiredNetworkImpl(reachable));
 | 
			
		||||
            // Otherwise we need to create our new networks.
 | 
			
		||||
            var networks = new ArrayList<WiredNetworkImpl>(1 + nodeSets.size());
 | 
			
		||||
            // Add the network we've created for the removed node.
 | 
			
		||||
            networks.add(wiredNetwork);
 | 
			
		||||
            //  And then create a new network for each disjoint subset.
 | 
			
		||||
            for (var set : nodeSets) {
 | 
			
		||||
                var network = new WiredNetworkImpl(new HashSet<>(set.size()));
 | 
			
		||||
                set.setNetwork(network);
 | 
			
		||||
                networks.add(network);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            for (var network : maximals) network.lock.writeLock().lock();
 | 
			
		||||
            for (var network : networks) network.lock.writeLock().lock();
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                // We special case the original node: detaching all peripherals when needed.
 | 
			
		||||
                wired.network = wiredNetwork;
 | 
			
		||||
                wired.peripherals = Map.of();
 | 
			
		||||
                wired.neighbours.clear();
 | 
			
		||||
 | 
			
		||||
                // Add all nodes to their appropriate network.
 | 
			
		||||
                for (var child : queue) {
 | 
			
		||||
                    var network = Nullability.assertNonNull(child.currentSet).network();
 | 
			
		||||
                    child.currentSet = null;
 | 
			
		||||
 | 
			
		||||
                // Ensure every network is finalised
 | 
			
		||||
                for (var network : maximals) {
 | 
			
		||||
                    for (var child : network.nodes) {
 | 
			
		||||
                    child.network = network;
 | 
			
		||||
                    network.nodes.add(child);
 | 
			
		||||
                    network.peripherals.putAll(child.peripherals);
 | 
			
		||||
                }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                for (var network : maximals) InvariantChecker.checkNetwork(network);
 | 
			
		||||
                for (var network : networks) InvariantChecker.checkNetwork(network);
 | 
			
		||||
                InvariantChecker.checkNode(wired);
 | 
			
		||||
 | 
			
		||||
                // Then broadcast network changes once all nodes are finalised
 | 
			
		||||
                for (var network : maximals) {
 | 
			
		||||
                for (var network : networks) {
 | 
			
		||||
                    WiredNetworkChangeImpl.changeOf(peripherals, network.peripherals).broadcast(network.nodes);
 | 
			
		||||
                }
 | 
			
		||||
            } finally {
 | 
			
		||||
                for (var network : maximals) network.lock.writeLock().unlock();
 | 
			
		||||
                for (var network : networks) network.lock.writeLock().unlock();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            nodes.clear();
 | 
			
		||||
@@ -373,22 +443,4 @@ final class WiredNetworkImpl implements WiredNetwork {
 | 
			
		||||
            throw new IllegalArgumentException("Unknown implementation of IWiredNode: " + node);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static Set<WiredNodeImpl> reachableNodes(WiredNodeImpl start) {
 | 
			
		||||
        Queue<WiredNodeImpl> enqueued = new ArrayDeque<>();
 | 
			
		||||
        var reachable = new HashSet<WiredNodeImpl>();
 | 
			
		||||
 | 
			
		||||
        reachable.add(start);
 | 
			
		||||
        enqueued.add(start);
 | 
			
		||||
 | 
			
		||||
        WiredNodeImpl node;
 | 
			
		||||
        while ((node = enqueued.poll()) != null) {
 | 
			
		||||
            for (var neighbour : node.neighbours) {
 | 
			
		||||
                // Otherwise attempt to enqueue this neighbour as well.
 | 
			
		||||
                if (reachable.add(neighbour)) enqueued.add(neighbour);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return reachable;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -27,11 +27,39 @@ public final class WiredNodeImpl implements WiredNode {
 | 
			
		||||
    final HashSet<WiredNodeImpl> neighbours = new HashSet<>();
 | 
			
		||||
    volatile WiredNetworkImpl network;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A temporary field used when checking network connectivity.
 | 
			
		||||
     *
 | 
			
		||||
     * @see WiredNetworkImpl#remove(WiredNode)
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    NodeSet currentSet;
 | 
			
		||||
 | 
			
		||||
    public WiredNodeImpl(WiredElement element) {
 | 
			
		||||
        this.element = element;
 | 
			
		||||
        network = new WiredNetworkImpl(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean connectTo(WiredNode node) {
 | 
			
		||||
        return network.connect(this, node);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean disconnectFrom(WiredNode node) {
 | 
			
		||||
        return network == ((WiredNodeImpl) node).network && network.disconnect(this, node);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean remove() {
 | 
			
		||||
        return network.remove(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void updatePeripherals(Map<String, IPeripheral> peripherals) {
 | 
			
		||||
        network.updatePeripherals(this, peripherals);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public synchronized void addReceiver(PacketReceiver receiver) {
 | 
			
		||||
        if (receivers == null) receivers = new HashSet<>();
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ import net.minecraft.resources.ResourceKey;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.server.MinecraftServer;
 | 
			
		||||
import net.minecraft.server.dedicated.DedicatedServer;
 | 
			
		||||
import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
import net.minecraft.server.level.ServerPlayer;
 | 
			
		||||
import net.minecraft.server.packs.resources.PreparableReloadListener;
 | 
			
		||||
import net.minecraft.world.entity.Entity;
 | 
			
		||||
@@ -78,10 +79,19 @@ public final class CommonHooks {
 | 
			
		||||
        NetworkUtils.reset();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void onServerChunkUnload(LevelChunk chunk) {
 | 
			
		||||
        if (!(chunk.getLevel() instanceof ServerLevel)) throw new IllegalArgumentException("Not a server chunk.");
 | 
			
		||||
        TickScheduler.onChunkUnload(chunk);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void onChunkWatch(LevelChunk chunk, ServerPlayer player) {
 | 
			
		||||
        MonitorWatcher.onWatch(chunk, player);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void onChunkTicketLevelChanged(ServerLevel level, long chunkPos, int oldLevel, int newLevel) {
 | 
			
		||||
        TickScheduler.onChunkTicketChanged(level, chunkPos, oldLevel, newLevel);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static final ResourceLocation TREASURE_DISK_LOOT = new ResourceLocation(ComputerCraftAPI.MOD_ID, "treasure_disk");
 | 
			
		||||
 | 
			
		||||
    private static final Set<ResourceLocation> TREASURE_DISK_LOOT_TABLES = Set.of(
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,6 @@ import dan200.computercraft.impl.PocketUpgrades;
 | 
			
		||||
import dan200.computercraft.impl.TurtleUpgrades;
 | 
			
		||||
import dan200.computercraft.shared.command.UserLevel;
 | 
			
		||||
import dan200.computercraft.shared.command.arguments.ComputerArgumentType;
 | 
			
		||||
import dan200.computercraft.shared.command.arguments.ComputersArgumentType;
 | 
			
		||||
import dan200.computercraft.shared.command.arguments.RepeatArgumentType;
 | 
			
		||||
import dan200.computercraft.shared.command.arguments.TrackingFieldArgumentType;
 | 
			
		||||
import dan200.computercraft.shared.common.ClearColourRecipe;
 | 
			
		||||
@@ -338,8 +337,7 @@ public final class ModRegistry {
 | 
			
		||||
 | 
			
		||||
        static {
 | 
			
		||||
            register("tracking_field", TrackingFieldArgumentType.class, TrackingFieldArgumentType.metric());
 | 
			
		||||
            register("computer", ComputerArgumentType.class, ComputerArgumentType.oneComputer());
 | 
			
		||||
            register("computers", ComputersArgumentType.class, new ComputersArgumentType.Info());
 | 
			
		||||
            register("computer", ComputerArgumentType.class, ComputerArgumentType.get());
 | 
			
		||||
            registerUnsafe("repeat", RepeatArgumentType.class, new RepeatArgumentType.Info());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,8 @@ import com.mojang.brigadier.suggestion.Suggestions;
 | 
			
		||||
import dan200.computercraft.core.computer.ComputerSide;
 | 
			
		||||
import dan200.computercraft.core.metrics.Metrics;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import dan200.computercraft.shared.command.arguments.ComputersArgumentType;
 | 
			
		||||
import dan200.computercraft.shared.command.arguments.ComputerArgumentType;
 | 
			
		||||
import dan200.computercraft.shared.command.arguments.ComputerSelector;
 | 
			
		||||
import dan200.computercraft.shared.command.text.TableBuilder;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ServerComputer;
 | 
			
		||||
@@ -23,6 +24,7 @@ import dan200.computercraft.shared.computer.metrics.basic.AggregatedMetric;
 | 
			
		||||
import dan200.computercraft.shared.computer.metrics.basic.BasicComputerMetricsObserver;
 | 
			
		||||
import dan200.computercraft.shared.computer.metrics.basic.ComputerMetrics;
 | 
			
		||||
import dan200.computercraft.shared.network.container.ComputerContainerData;
 | 
			
		||||
import net.minecraft.ChatFormatting;
 | 
			
		||||
import net.minecraft.commands.CommandSourceStack;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
@@ -42,9 +44,6 @@ import java.util.*;
 | 
			
		||||
import static dan200.computercraft.shared.command.CommandUtils.isPlayer;
 | 
			
		||||
import static dan200.computercraft.shared.command.Exceptions.NOT_TRACKING_EXCEPTION;
 | 
			
		||||
import static dan200.computercraft.shared.command.Exceptions.NO_TIMINGS_EXCEPTION;
 | 
			
		||||
import static dan200.computercraft.shared.command.arguments.ComputerArgumentType.getComputerArgument;
 | 
			
		||||
import static dan200.computercraft.shared.command.arguments.ComputerArgumentType.oneComputer;
 | 
			
		||||
import static dan200.computercraft.shared.command.arguments.ComputersArgumentType.*;
 | 
			
		||||
import static dan200.computercraft.shared.command.arguments.TrackingFieldArgumentType.metric;
 | 
			
		||||
import static dan200.computercraft.shared.command.builder.CommandBuilder.args;
 | 
			
		||||
import static dan200.computercraft.shared.command.builder.CommandBuilder.command;
 | 
			
		||||
@@ -70,37 +69,37 @@ public final class CommandComputerCraft {
 | 
			
		||||
                .requires(ModRegistry.Permissions.PERMISSION_DUMP)
 | 
			
		||||
                .executes(c -> dump(c.getSource()))
 | 
			
		||||
                .then(args()
 | 
			
		||||
                    .arg("computer", oneComputer())
 | 
			
		||||
                    .executes(c -> dumpComputer(c.getSource(), getComputerArgument(c, "computer")))))
 | 
			
		||||
                    .arg("computer", ComputerArgumentType.get())
 | 
			
		||||
                    .executes(c -> dumpComputer(c.getSource(), ComputerArgumentType.getOne(c, "computer")))))
 | 
			
		||||
 | 
			
		||||
            .then(command("shutdown")
 | 
			
		||||
                .requires(ModRegistry.Permissions.PERMISSION_SHUTDOWN)
 | 
			
		||||
                .argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers())
 | 
			
		||||
                .argManyValue("computers", ComputerArgumentType.get(), ComputerSelector.all())
 | 
			
		||||
                .executes((c, a) -> shutdown(c.getSource(), unwrap(c.getSource(), a))))
 | 
			
		||||
 | 
			
		||||
            .then(command("turn-on")
 | 
			
		||||
                .requires(ModRegistry.Permissions.PERMISSION_TURN_ON)
 | 
			
		||||
                .argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers())
 | 
			
		||||
                .argManyValue("computers", ComputerArgumentType.get(), ComputerSelector.all())
 | 
			
		||||
                .executes((c, a) -> turnOn(c.getSource(), unwrap(c.getSource(), a))))
 | 
			
		||||
 | 
			
		||||
            .then(command("tp")
 | 
			
		||||
                .requires(ModRegistry.Permissions.PERMISSION_TP)
 | 
			
		||||
                .arg("computer", oneComputer())
 | 
			
		||||
                .executes(c -> teleport(c.getSource(), getComputerArgument(c, "computer"))))
 | 
			
		||||
                .arg("computer", ComputerArgumentType.get())
 | 
			
		||||
                .executes(c -> teleport(c.getSource(), ComputerArgumentType.getOne(c, "computer"))))
 | 
			
		||||
 | 
			
		||||
            .then(command("queue")
 | 
			
		||||
                .requires(ModRegistry.Permissions.PERMISSION_QUEUE)
 | 
			
		||||
                .arg(
 | 
			
		||||
                    RequiredArgumentBuilder.<CommandSourceStack, ComputersArgumentType.ComputersSupplier>argument("computer", manyComputers())
 | 
			
		||||
                    RequiredArgumentBuilder.<CommandSourceStack, ComputerSelector>argument("computer", ComputerArgumentType.get())
 | 
			
		||||
                        .suggests((context, builder) -> Suggestions.empty())
 | 
			
		||||
                )
 | 
			
		||||
                .argManyValue("args", StringArgumentType.string(), List.of())
 | 
			
		||||
                .executes((c, a) -> queue(getComputersArgument(c, "computer"), a)))
 | 
			
		||||
                .executes((c, a) -> queue(ComputerArgumentType.getMany(c, "computer"), a)))
 | 
			
		||||
 | 
			
		||||
            .then(command("view")
 | 
			
		||||
                .requires(ModRegistry.Permissions.PERMISSION_VIEW)
 | 
			
		||||
                .arg("computer", oneComputer())
 | 
			
		||||
                .executes(c -> view(c.getSource(), getComputerArgument(c, "computer"))))
 | 
			
		||||
                .arg("computer", ComputerArgumentType.get())
 | 
			
		||||
                .executes(c -> view(c.getSource(), ComputerArgumentType.getOne(c, "computer"))))
 | 
			
		||||
 | 
			
		||||
            .then(choice("track")
 | 
			
		||||
                .requires(ModRegistry.Permissions.PERMISSION_TRACK)
 | 
			
		||||
@@ -135,7 +134,7 @@ public final class CommandComputerCraft {
 | 
			
		||||
            } else if (b.getLevel() == world) {
 | 
			
		||||
                return 1;
 | 
			
		||||
            } else {
 | 
			
		||||
                return Integer.compare(a.getInstanceID(), b.getInstanceID());
 | 
			
		||||
                return a.getInstanceUUID().compareTo(b.getInstanceUUID());
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@@ -160,7 +159,8 @@ public final class CommandComputerCraft {
 | 
			
		||||
     */
 | 
			
		||||
    private static int dumpComputer(CommandSourceStack source, ServerComputer computer) {
 | 
			
		||||
        var table = new TableBuilder("Dump");
 | 
			
		||||
        table.row(header("Instance"), text(Integer.toString(computer.getInstanceID())));
 | 
			
		||||
        table.row(header("Instance ID"), text(Integer.toString(computer.getInstanceID())));
 | 
			
		||||
        table.row(header("Instance UUID"), text(computer.getInstanceUUID().toString()));
 | 
			
		||||
        table.row(header("Id"), text(Integer.toString(computer.getID())));
 | 
			
		||||
        table.row(header("Label"), text(computer.getLabel()));
 | 
			
		||||
        table.row(header("On"), bool(computer.isOn()));
 | 
			
		||||
@@ -332,29 +332,22 @@ public final class CommandComputerCraft {
 | 
			
		||||
 | 
			
		||||
    // Additional helper functions.
 | 
			
		||||
 | 
			
		||||
    private static Component linkComputer(CommandSourceStack source, @Nullable ServerComputer serverComputer, int computerId) {
 | 
			
		||||
    private static Component linkComputer(CommandSourceStack source, @Nullable ServerComputer computer, int computerId) {
 | 
			
		||||
        var out = Component.literal("");
 | 
			
		||||
 | 
			
		||||
        // Append the computer instance
 | 
			
		||||
        if (serverComputer == null) {
 | 
			
		||||
            out.append(text("?"));
 | 
			
		||||
        // And instance
 | 
			
		||||
        if (computer == null) {
 | 
			
		||||
            out.append("#" + computerId + " ").append(coloured("(unloaded)", ChatFormatting.GRAY));
 | 
			
		||||
        } else {
 | 
			
		||||
            out.append(link(
 | 
			
		||||
                text(Integer.toString(serverComputer.getInstanceID())),
 | 
			
		||||
                "/computercraft dump " + serverComputer.getInstanceID(),
 | 
			
		||||
                Component.translatable("commands.computercraft.dump.action")
 | 
			
		||||
            ));
 | 
			
		||||
            out.append(makeComputerDumpCommand(computer));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // And ID
 | 
			
		||||
        out.append(" (id " + computerId + ")");
 | 
			
		||||
 | 
			
		||||
        // And, if we're a player, some useful links
 | 
			
		||||
        if (serverComputer != null && isPlayer(source)) {
 | 
			
		||||
        if (computer != null && isPlayer(source)) {
 | 
			
		||||
            if (ModRegistry.Permissions.PERMISSION_TP.test(source)) {
 | 
			
		||||
                out.append(" ").append(link(
 | 
			
		||||
                    text("\u261b"),
 | 
			
		||||
                    "/computercraft tp " + serverComputer.getInstanceID(),
 | 
			
		||||
                    makeComputerCommand("tp", computer),
 | 
			
		||||
                    Component.translatable("commands.computercraft.tp.action")
 | 
			
		||||
                ));
 | 
			
		||||
            }
 | 
			
		||||
@@ -362,7 +355,7 @@ public final class CommandComputerCraft {
 | 
			
		||||
            if (ModRegistry.Permissions.PERMISSION_VIEW.test(source)) {
 | 
			
		||||
                out.append(" ").append(link(
 | 
			
		||||
                    text("\u20e2"),
 | 
			
		||||
                    "/computercraft view " + serverComputer.getInstanceID(),
 | 
			
		||||
                    makeComputerCommand("view", computer),
 | 
			
		||||
                    Component.translatable("commands.computercraft.view.action")
 | 
			
		||||
                ));
 | 
			
		||||
            }
 | 
			
		||||
@@ -380,7 +373,7 @@ public final class CommandComputerCraft {
 | 
			
		||||
        if (ModRegistry.Permissions.PERMISSION_TP.test(context)) {
 | 
			
		||||
            return link(
 | 
			
		||||
                position(computer.getPosition()),
 | 
			
		||||
                "/computercraft tp " + computer.getInstanceID(),
 | 
			
		||||
                makeComputerCommand("tp", computer),
 | 
			
		||||
                Component.translatable("commands.computercraft.tp.action")
 | 
			
		||||
            );
 | 
			
		||||
        } else {
 | 
			
		||||
@@ -392,7 +385,7 @@ public final class CommandComputerCraft {
 | 
			
		||||
        var file = new File(ServerContext.get(source.getServer()).storageDir().toFile(), "computer/" + id);
 | 
			
		||||
        if (!file.isDirectory()) return null;
 | 
			
		||||
 | 
			
		||||
        return link(
 | 
			
		||||
        return clientLink(
 | 
			
		||||
            text("\u270E"),
 | 
			
		||||
            "/" + CLIENT_OPEN_FOLDER + " " + id,
 | 
			
		||||
            Component.translatable("commands.computercraft.dump.open_path")
 | 
			
		||||
@@ -431,4 +424,10 @@ public final class CommandComputerCraft {
 | 
			
		||||
        table.display(source);
 | 
			
		||||
        return timings.size();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static Set<ServerComputer> unwrap(CommandSourceStack source, Collection<ComputerSelector> suppliers) {
 | 
			
		||||
        Set<ServerComputer> computers = new HashSet<>();
 | 
			
		||||
        for (var supplier : suppliers) supplier.find(source).forEach(computers::add);
 | 
			
		||||
        return computers;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -28,12 +28,12 @@ public final class CommandUtils {
 | 
			
		||||
    @SuppressWarnings("unchecked")
 | 
			
		||||
    public static CompletableFuture<Suggestions> suggestOnServer(CommandContext<?> context, Function<CommandContext<CommandSourceStack>, CompletableFuture<Suggestions>> supplier) {
 | 
			
		||||
        var source = context.getSource();
 | 
			
		||||
        if (!(source instanceof SharedSuggestionProvider)) {
 | 
			
		||||
        if (!(source instanceof SharedSuggestionProvider shared)) {
 | 
			
		||||
            return Suggestions.empty();
 | 
			
		||||
        } else if (source instanceof CommandSourceStack) {
 | 
			
		||||
            return supplier.apply((CommandContext<CommandSourceStack>) context);
 | 
			
		||||
        } else {
 | 
			
		||||
            return ((SharedSuggestionProvider) source).customSuggestion(context);
 | 
			
		||||
            return shared.customSuggestion(context);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,8 @@ package dan200.computercraft.shared.command;
 | 
			
		||||
import com.mojang.brigadier.exceptions.Dynamic2CommandExceptionType;
 | 
			
		||||
import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;
 | 
			
		||||
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
 | 
			
		||||
import net.minecraft.commands.arguments.selector.EntitySelectorParser;
 | 
			
		||||
import net.minecraft.commands.arguments.selector.options.EntitySelectorOptions;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
 | 
			
		||||
public final class Exceptions {
 | 
			
		||||
@@ -20,6 +22,13 @@ public final class Exceptions {
 | 
			
		||||
 | 
			
		||||
    public static final SimpleCommandExceptionType ARGUMENT_EXPECTED = translated("argument.computercraft.argument_expected");
 | 
			
		||||
 | 
			
		||||
    public static final DynamicCommandExceptionType UNKNOWN_FAMILY = translated1("argument.computercraft.unknown_computer_family");
 | 
			
		||||
 | 
			
		||||
    public static final DynamicCommandExceptionType ERROR_EXPECTED_OPTION_VALUE = EntitySelectorParser.ERROR_EXPECTED_OPTION_VALUE;
 | 
			
		||||
    public static final SimpleCommandExceptionType ERROR_EXPECTED_END_OF_OPTIONS = EntitySelectorParser.ERROR_EXPECTED_END_OF_OPTIONS;
 | 
			
		||||
    public static final DynamicCommandExceptionType ERROR_UNKNOWN_OPTION = EntitySelectorOptions.ERROR_UNKNOWN_OPTION;
 | 
			
		||||
    public static final DynamicCommandExceptionType ERROR_INAPPLICABLE_OPTION = EntitySelectorOptions.ERROR_INAPPLICABLE_OPTION;
 | 
			
		||||
 | 
			
		||||
    private static SimpleCommandExceptionType translated(String key) {
 | 
			
		||||
        return new SimpleCommandExceptionType(Component.translatable(key));
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,29 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.command.arguments;
 | 
			
		||||
 | 
			
		||||
import com.mojang.brigadier.StringReader;
 | 
			
		||||
 | 
			
		||||
final class ArgumentParserUtils {
 | 
			
		||||
    private ArgumentParserUtils() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static boolean consume(StringReader reader, char lookahead) {
 | 
			
		||||
        if (!reader.canRead() || reader.peek() != lookahead) return false;
 | 
			
		||||
 | 
			
		||||
        reader.skip();
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static boolean consume(StringReader reader, String lookahead) {
 | 
			
		||||
        if (!reader.canRead(lookahead.length())) return false;
 | 
			
		||||
        for (var i = 0; i < lookahead.length(); i++) {
 | 
			
		||||
            if (reader.peek(i) != lookahead.charAt(i)) return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        reader.setCursor(reader.getCursor() + lookahead.length());
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -14,66 +14,58 @@ import dan200.computercraft.shared.computer.core.ServerComputer;
 | 
			
		||||
import net.minecraft.commands.CommandSourceStack;
 | 
			
		||||
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.shared.command.Exceptions.COMPUTER_ARG_MANY;
 | 
			
		||||
 | 
			
		||||
public final class ComputerArgumentType implements ArgumentType<ComputerArgumentType.ComputerSupplier> {
 | 
			
		||||
public final class ComputerArgumentType implements ArgumentType<ComputerSelector> {
 | 
			
		||||
    private static final ComputerArgumentType INSTANCE = new ComputerArgumentType();
 | 
			
		||||
 | 
			
		||||
    public static ComputerArgumentType oneComputer() {
 | 
			
		||||
        return INSTANCE;
 | 
			
		||||
    }
 | 
			
		||||
    private static final List<String> EXAMPLES = List.of(
 | 
			
		||||
        "0", "123", "@c[instance_id=123]"
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    public static ServerComputer getComputerArgument(CommandContext<CommandSourceStack> context, String name) throws CommandSyntaxException {
 | 
			
		||||
        return context.getArgument(name, ComputerSupplier.class).unwrap(context.getSource());
 | 
			
		||||
    public static ComputerArgumentType get() {
 | 
			
		||||
        return INSTANCE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ComputerArgumentType() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Extract a list of computers from a {@link CommandContext} argument.
 | 
			
		||||
     *
 | 
			
		||||
     * @param context The current command context.
 | 
			
		||||
     * @param name    The name of the argument.
 | 
			
		||||
     * @return The found computer(s).
 | 
			
		||||
     */
 | 
			
		||||
    public static List<ServerComputer> getMany(CommandContext<CommandSourceStack> context, String name) {
 | 
			
		||||
        return context.getArgument(name, ComputerSelector.class).find(context.getSource()).toList();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Extract a single computer from a {@link CommandContext} argument.
 | 
			
		||||
     *
 | 
			
		||||
     * @param context The current command context.
 | 
			
		||||
     * @param name    The name of the argument.
 | 
			
		||||
     * @return The found computer.
 | 
			
		||||
     * @throws CommandSyntaxException If exactly one computer could not be found.
 | 
			
		||||
     */
 | 
			
		||||
    public static ServerComputer getOne(CommandContext<CommandSourceStack> context, String name) throws CommandSyntaxException {
 | 
			
		||||
        return context.getArgument(name, ComputerSelector.class).findOne(context.getSource());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public ComputerSupplier parse(StringReader reader) throws CommandSyntaxException {
 | 
			
		||||
        var start = reader.getCursor();
 | 
			
		||||
        var supplier = ComputersArgumentType.someComputers().parse(reader);
 | 
			
		||||
        var selector = reader.getString().substring(start, reader.getCursor());
 | 
			
		||||
 | 
			
		||||
        return s -> {
 | 
			
		||||
            var computers = supplier.unwrap(s);
 | 
			
		||||
 | 
			
		||||
            if (computers.size() == 1) return computers.iterator().next();
 | 
			
		||||
 | 
			
		||||
            var builder = new StringBuilder();
 | 
			
		||||
            var first = true;
 | 
			
		||||
            for (var computer : computers) {
 | 
			
		||||
                if (first) {
 | 
			
		||||
                    first = false;
 | 
			
		||||
                } else {
 | 
			
		||||
                    builder.append(", ");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                builder.append(computer.getInstanceID());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            // We have an incorrect number of computers: reset and throw an error
 | 
			
		||||
            reader.setCursor(start);
 | 
			
		||||
            throw COMPUTER_ARG_MANY.createWithContext(reader, selector, builder.toString());
 | 
			
		||||
        };
 | 
			
		||||
    public ComputerSelector parse(StringReader reader) throws CommandSyntaxException {
 | 
			
		||||
        return ComputerSelector.parse(reader);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
 | 
			
		||||
        return ComputersArgumentType.someComputers().listSuggestions(context, builder);
 | 
			
		||||
        return ComputerSelector.suggest(context, builder);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Collection<String> getExamples() {
 | 
			
		||||
        return ComputersArgumentType.someComputers().getExamples();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @FunctionalInterface
 | 
			
		||||
    public interface ComputerSupplier {
 | 
			
		||||
        ServerComputer unwrap(CommandSourceStack source) throws CommandSyntaxException;
 | 
			
		||||
        return EXAMPLES;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,387 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.command.arguments;
 | 
			
		||||
 | 
			
		||||
import com.mojang.brigadier.Message;
 | 
			
		||||
import com.mojang.brigadier.StringReader;
 | 
			
		||||
import com.mojang.brigadier.context.CommandContext;
 | 
			
		||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
 | 
			
		||||
import com.mojang.brigadier.suggestion.Suggestions;
 | 
			
		||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ServerComputer;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ServerContext;
 | 
			
		||||
import net.minecraft.advancements.critereon.MinMaxBounds;
 | 
			
		||||
import net.minecraft.commands.CommandSourceStack;
 | 
			
		||||
import net.minecraft.commands.SharedSuggestionProvider;
 | 
			
		||||
import net.minecraft.commands.arguments.UuidArgument;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.world.phys.AABB;
 | 
			
		||||
import net.minecraft.world.phys.Vec3;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
import java.util.function.Function;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
import java.util.stream.Stream;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.shared.command.CommandUtils.suggestOnServer;
 | 
			
		||||
import static dan200.computercraft.shared.command.Exceptions.*;
 | 
			
		||||
import static dan200.computercraft.shared.command.arguments.ArgumentParserUtils.consume;
 | 
			
		||||
import static dan200.computercraft.shared.command.text.ChatHelpers.makeComputerDumpCommand;
 | 
			
		||||
 | 
			
		||||
public record ComputerSelector(
 | 
			
		||||
    String selector,
 | 
			
		||||
    OptionalInt instanceId,
 | 
			
		||||
    @Nullable UUID instanceUuid,
 | 
			
		||||
    OptionalInt computerId,
 | 
			
		||||
    @Nullable String label,
 | 
			
		||||
    @Nullable ComputerFamily family,
 | 
			
		||||
    @Nullable AABB bounds,
 | 
			
		||||
    @Nullable MinMaxBounds.Doubles range
 | 
			
		||||
) {
 | 
			
		||||
    private static final ComputerSelector all = new ComputerSelector("@c[]", OptionalInt.empty(), null, OptionalInt.empty(), null, null, null, null);
 | 
			
		||||
 | 
			
		||||
    private static UuidArgument uuidArgument = UuidArgument.uuid();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A {@link ComputerSelector} which matches all computers.
 | 
			
		||||
     *
 | 
			
		||||
     * @return A {@link ComputerSelector} instance.
 | 
			
		||||
     */
 | 
			
		||||
    public static ComputerSelector all() {
 | 
			
		||||
        return all;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Find all computers matching this selector.
 | 
			
		||||
     *
 | 
			
		||||
     * @param source The source requesting these computers.
 | 
			
		||||
     * @return The stream of matching computers.
 | 
			
		||||
     */
 | 
			
		||||
    public Stream<ServerComputer> find(CommandSourceStack source) {
 | 
			
		||||
        var context = ServerContext.get(source.getServer());
 | 
			
		||||
        if (instanceId().isPresent()) {
 | 
			
		||||
            var computer = context.registry().get(instanceId().getAsInt());
 | 
			
		||||
            return computer != null && matches(source, computer) ? Stream.of(computer) : Stream.of();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (instanceUuid() != null) {
 | 
			
		||||
            var computer = context.registry().get(instanceUuid());
 | 
			
		||||
            return computer != null && matches(source, computer) ? Stream.of(computer) : Stream.of();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return context.registry().getComputers().stream().filter(c -> matches(source, c));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Find exactly one computer which matches this selector.
 | 
			
		||||
     *
 | 
			
		||||
     * @param source The source requesting this computer.
 | 
			
		||||
     * @return The computer.
 | 
			
		||||
     * @throws CommandSyntaxException If no or multiple computers could be found.
 | 
			
		||||
     */
 | 
			
		||||
    public ServerComputer findOne(CommandSourceStack source) throws CommandSyntaxException {
 | 
			
		||||
        var computers = find(source).toList();
 | 
			
		||||
        if (computers.isEmpty()) throw COMPUTER_ARG_NONE.create(selector);
 | 
			
		||||
        if (computers.size() == 1) return computers.iterator().next();
 | 
			
		||||
 | 
			
		||||
        var builder = Component.empty();
 | 
			
		||||
        var first = true;
 | 
			
		||||
        for (var computer : computers) {
 | 
			
		||||
            if (first) {
 | 
			
		||||
                first = false;
 | 
			
		||||
            } else {
 | 
			
		||||
                builder.append(", ");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            builder.append(makeComputerDumpCommand(computer));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // We have an incorrect number of computers: throw an error
 | 
			
		||||
        throw COMPUTER_ARG_MANY.create(selector, builder);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Determine if this selector matches a given computer.
 | 
			
		||||
     *
 | 
			
		||||
     * @param source   The command source, used for distance comparisons.
 | 
			
		||||
     * @param computer The computer to check.
 | 
			
		||||
     * @return If this computer is matched by the selector.
 | 
			
		||||
     */
 | 
			
		||||
    public boolean matches(CommandSourceStack source, ServerComputer computer) {
 | 
			
		||||
        return (instanceId().isEmpty() || computer.getInstanceID() == instanceId().getAsInt())
 | 
			
		||||
            && (instanceUuid() == null || computer.getInstanceUUID().equals(instanceUuid()))
 | 
			
		||||
            && (computerId().isEmpty() || computer.getID() == computerId().getAsInt())
 | 
			
		||||
            && (label == null || Objects.equals(computer.getLabel(), label))
 | 
			
		||||
            && (family == null || computer.getFamily() == family)
 | 
			
		||||
            && (bounds == null || (source.getLevel() == computer.getLevel() && bounds.contains(Vec3.atCenterOf(computer.getPosition()))))
 | 
			
		||||
            && (range == null || (source.getLevel() == computer.getLevel() && range.matchesSqr(source.getPosition().distanceToSqr(Vec3.atCenterOf(computer.getPosition())))));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parse an input string.
 | 
			
		||||
     *
 | 
			
		||||
     * @param reader The reader to parse from.
 | 
			
		||||
     * @return The parsed selector.
 | 
			
		||||
     * @throws CommandSyntaxException If the selector was incomplete or malformed.
 | 
			
		||||
     */
 | 
			
		||||
    public static ComputerSelector parse(StringReader reader) throws CommandSyntaxException {
 | 
			
		||||
        var start = reader.getCursor();
 | 
			
		||||
 | 
			
		||||
        var builder = new Builder();
 | 
			
		||||
 | 
			
		||||
        if (consume(reader, "@c[")) {
 | 
			
		||||
            parseSelector(builder, reader);
 | 
			
		||||
        } else {
 | 
			
		||||
            // TODO(1.20.5): Only parse computer ids here.
 | 
			
		||||
            var kind = reader.peek();
 | 
			
		||||
            if (kind == '@') {
 | 
			
		||||
                reader.skip();
 | 
			
		||||
                builder.label = reader.readString();
 | 
			
		||||
            } else if (kind == '~') {
 | 
			
		||||
                reader.skip();
 | 
			
		||||
                builder.family = parseFamily(reader);
 | 
			
		||||
            } else if (kind == '#') {
 | 
			
		||||
                reader.skip();
 | 
			
		||||
                builder.computerId = OptionalInt.of(reader.readInt());
 | 
			
		||||
            } else {
 | 
			
		||||
                builder.instanceId = OptionalInt.of(reader.readInt());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var selector = reader.getString().substring(start, reader.getCursor());
 | 
			
		||||
        return new ComputerSelector(selector, builder.instanceId, builder.instanceUuid, builder.computerId, builder.label, builder.family, builder.bounds, builder.range);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void parseSelector(Builder builder, StringReader reader) throws CommandSyntaxException {
 | 
			
		||||
        Set<Option> seenOptions = new HashSet<>();
 | 
			
		||||
        while (true) {
 | 
			
		||||
            reader.skipWhitespace();
 | 
			
		||||
 | 
			
		||||
            if (!reader.canRead()) throw ERROR_EXPECTED_END_OF_OPTIONS.createWithContext(reader);
 | 
			
		||||
            if (consume(reader, ']')) break;
 | 
			
		||||
 | 
			
		||||
            // Read the option and validate it.
 | 
			
		||||
            var option = parseOption(reader, seenOptions);
 | 
			
		||||
            reader.skipWhitespace();
 | 
			
		||||
            if (!consume(reader, '=')) throw ERROR_EXPECTED_OPTION_VALUE.createWithContext(reader, option.name());
 | 
			
		||||
            reader.skipWhitespace();
 | 
			
		||||
            option.parser.parse(reader, builder);
 | 
			
		||||
            reader.skipWhitespace();
 | 
			
		||||
 | 
			
		||||
            if (consume(reader, ']')) break;
 | 
			
		||||
            if (!consume(reader, ',')) throw ERROR_EXPECTED_END_OF_OPTIONS.createWithContext(reader);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static Option parseOption(StringReader reader, Set<Option> seen) throws CommandSyntaxException {
 | 
			
		||||
        var start = reader.getCursor();
 | 
			
		||||
        var name = reader.readUnquotedString();
 | 
			
		||||
        var option = options.get(name);
 | 
			
		||||
        if (option == null) {
 | 
			
		||||
            reader.setCursor(start);
 | 
			
		||||
            throw ERROR_UNKNOWN_OPTION.createWithContext(reader, name);
 | 
			
		||||
        } else if (!seen.add(option)) {
 | 
			
		||||
            throw ERROR_INAPPLICABLE_OPTION.createWithContext(reader, name);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return option;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static ComputerFamily parseFamily(StringReader reader) throws CommandSyntaxException {
 | 
			
		||||
        var start = reader.getCursor();
 | 
			
		||||
        var name = reader.readUnquotedString();
 | 
			
		||||
        var family = Arrays.stream(ComputerFamily.values()).filter(x -> x.name().equalsIgnoreCase(name)).findFirst().orElse(null);
 | 
			
		||||
        if (family == null) {
 | 
			
		||||
            reader.setCursor(start);
 | 
			
		||||
            throw UNKNOWN_FAMILY.createWithContext(reader, name);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return family;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Suggest completions for a selector argument.
 | 
			
		||||
     *
 | 
			
		||||
     * @param context The current command context.
 | 
			
		||||
     * @param builder The builder containing the current input.
 | 
			
		||||
     * @return The possible suggestions.
 | 
			
		||||
     */
 | 
			
		||||
    public static CompletableFuture<Suggestions> suggest(CommandContext<?> context, SuggestionsBuilder builder) {
 | 
			
		||||
        var remaining = builder.getRemaining();
 | 
			
		||||
 | 
			
		||||
        if (remaining.startsWith("@")) {
 | 
			
		||||
            var reader = new StringReader(builder.getInput());
 | 
			
		||||
            reader.setCursor(builder.getStart());
 | 
			
		||||
            return suggestSelector(context, reader);
 | 
			
		||||
        } else if (remaining.startsWith("#")) {
 | 
			
		||||
            return suggestComputers(c -> "#" + c.getID()).suggest(context, builder);
 | 
			
		||||
        } else {
 | 
			
		||||
            return suggestComputers(c -> Integer.toString(c.getInstanceID())).suggest(context, builder);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static CompletableFuture<Suggestions> suggestSelector(CommandContext<?> context, StringReader reader) {
 | 
			
		||||
        Set<Option> seenOptions = new HashSet<>();
 | 
			
		||||
        var builder = new Builder();
 | 
			
		||||
 | 
			
		||||
        if (!consume(reader, "@c[")) return suggestions(reader).suggest("@c[").buildFuture();
 | 
			
		||||
 | 
			
		||||
        while (true) {
 | 
			
		||||
            reader.skipWhitespace();
 | 
			
		||||
 | 
			
		||||
            if (!reader.canRead()) return suggestOptions(reader);
 | 
			
		||||
            if (consume(reader, ']')) break;
 | 
			
		||||
 | 
			
		||||
            // Read the option and validate it.
 | 
			
		||||
            Option option;
 | 
			
		||||
            try {
 | 
			
		||||
                option = parseOption(reader, seenOptions);
 | 
			
		||||
            } catch (CommandSyntaxException e) {
 | 
			
		||||
                return suggestOptions(reader);
 | 
			
		||||
            }
 | 
			
		||||
            reader.skipWhitespace();
 | 
			
		||||
            if (!consume(reader, '=')) return suggestions(reader).suggest("=").buildFuture();
 | 
			
		||||
            reader.skipWhitespace();
 | 
			
		||||
            try {
 | 
			
		||||
                option.parser.parse(reader, builder);
 | 
			
		||||
            } catch (CommandSyntaxException e) {
 | 
			
		||||
                return option.suggest.suggest(context, suggestions(reader));
 | 
			
		||||
            }
 | 
			
		||||
            reader.skipWhitespace();
 | 
			
		||||
 | 
			
		||||
            if (consume(reader, ']')) break;
 | 
			
		||||
            if (!consume(reader, ',')) return suggestions(reader).suggest(",").buildFuture();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Suggestions.empty();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static CompletableFuture<Suggestions> suggestOptions(StringReader reader) {
 | 
			
		||||
        return SharedSuggestionProvider.suggest(options().values(), suggestions(reader), Option::name, Option::tooltip);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static SuggestionsBuilder suggestions(StringReader reader) {
 | 
			
		||||
        return new SuggestionsBuilder(reader.getString(), reader.getCursor());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static final class Builder {
 | 
			
		||||
        private OptionalInt instanceId = OptionalInt.empty();
 | 
			
		||||
        private @Nullable UUID instanceUuid = null;
 | 
			
		||||
        private OptionalInt computerId = OptionalInt.empty();
 | 
			
		||||
        private @Nullable String label;
 | 
			
		||||
        private @Nullable ComputerFamily family;
 | 
			
		||||
        private @Nullable AABB bounds;
 | 
			
		||||
        private @Nullable MinMaxBounds.Doubles range;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static final Map<String, Option> options;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a map of individual selector options.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The available options.
 | 
			
		||||
     */
 | 
			
		||||
    public static Map<String, Option> options() {
 | 
			
		||||
        return options;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static {
 | 
			
		||||
        var optionList = new Option[]{
 | 
			
		||||
            new Option(
 | 
			
		||||
                "instance",
 | 
			
		||||
                (reader, builder) -> builder.instanceUuid = uuidArgument.parse(reader),
 | 
			
		||||
                suggestComputers(c -> c.getInstanceUUID().toString())
 | 
			
		||||
            ),
 | 
			
		||||
            new Option(
 | 
			
		||||
                "id",
 | 
			
		||||
                (reader, builder) -> builder.computerId = OptionalInt.of(reader.readInt()),
 | 
			
		||||
                suggestComputers(c -> Integer.toString(c.getID()))
 | 
			
		||||
            ),
 | 
			
		||||
            new Option(
 | 
			
		||||
                "label",
 | 
			
		||||
                (reader, builder) -> builder.label = reader.readQuotedString(),
 | 
			
		||||
                suggestComputers(ServerComputer::getLabel)
 | 
			
		||||
            ),
 | 
			
		||||
            new Option(
 | 
			
		||||
                "family",
 | 
			
		||||
                (reader, builder) -> builder.family = parseFamily(reader),
 | 
			
		||||
                (source, builder) -> SharedSuggestionProvider.suggest(Arrays.stream(ComputerFamily.values()).map(x -> x.name().toLowerCase(Locale.ROOT)), builder)
 | 
			
		||||
            ),
 | 
			
		||||
            new Option(
 | 
			
		||||
                "distance",
 | 
			
		||||
                (reader, builder) -> builder.range = MinMaxBounds.Doubles.fromReader(reader),
 | 
			
		||||
                (source, builder) -> Suggestions.empty()
 | 
			
		||||
            ),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        options = Arrays.stream(optionList).collect(Collectors.toUnmodifiableMap(Option::name, x -> x));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A single option to filter a computer by.
 | 
			
		||||
     */
 | 
			
		||||
    public static final class Option {
 | 
			
		||||
        private final String name;
 | 
			
		||||
        private final Parser parser;
 | 
			
		||||
        private final SuggestionProvider suggest;
 | 
			
		||||
        private final String translationKey;
 | 
			
		||||
        private final Message tooltip;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        Option(String name, Parser parser, SuggestionProvider suggest) {
 | 
			
		||||
            this.name = name;
 | 
			
		||||
            this.parser = parser;
 | 
			
		||||
            this.suggest = suggest;
 | 
			
		||||
            tooltip = Component.translatable(translationKey = "argument.computercraft.computer." + name);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * The name of this selector.
 | 
			
		||||
         *
 | 
			
		||||
         * @return The selector's name.
 | 
			
		||||
         */
 | 
			
		||||
        public String name() {
 | 
			
		||||
            return name;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Message tooltip() {
 | 
			
		||||
            return tooltip;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * The translation key for this selector.
 | 
			
		||||
         *
 | 
			
		||||
         * @return The selector's translation key.
 | 
			
		||||
         */
 | 
			
		||||
        public String translationKey() {
 | 
			
		||||
            return translationKey;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private interface Parser {
 | 
			
		||||
        void parse(StringReader reader, Builder builder) throws CommandSyntaxException;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private interface SuggestionProvider {
 | 
			
		||||
        CompletableFuture<Suggestions> suggest(CommandContext<?> source, SuggestionsBuilder builder);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static SuggestionProvider suggestComputers(Function<ServerComputer, String> renderer) {
 | 
			
		||||
        return (anyContext, builder) -> suggestOnServer(anyContext, context -> {
 | 
			
		||||
            var remaining = builder.getRemaining();
 | 
			
		||||
            for (var computer : ServerContext.get(context.getSource().getServer()).registry().getComputers()) {
 | 
			
		||||
                var converted = renderer.apply(computer);
 | 
			
		||||
                if (converted != null && converted.startsWith(remaining)) {
 | 
			
		||||
                    builder.suggest(converted);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return builder.buildFuture();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,188 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.command.arguments;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import com.mojang.brigadier.StringReader;
 | 
			
		||||
import com.mojang.brigadier.arguments.ArgumentType;
 | 
			
		||||
import com.mojang.brigadier.context.CommandContext;
 | 
			
		||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
 | 
			
		||||
import com.mojang.brigadier.suggestion.Suggestions;
 | 
			
		||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ServerComputer;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ServerContext;
 | 
			
		||||
import net.minecraft.commands.CommandBuildContext;
 | 
			
		||||
import net.minecraft.commands.CommandSourceStack;
 | 
			
		||||
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
 | 
			
		||||
import java.util.*;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
import java.util.function.Function;
 | 
			
		||||
import java.util.function.Predicate;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.shared.command.CommandUtils.suggest;
 | 
			
		||||
import static dan200.computercraft.shared.command.CommandUtils.suggestOnServer;
 | 
			
		||||
import static dan200.computercraft.shared.command.Exceptions.COMPUTER_ARG_NONE;
 | 
			
		||||
 | 
			
		||||
public final class ComputersArgumentType implements ArgumentType<ComputersArgumentType.ComputersSupplier> {
 | 
			
		||||
    private static final ComputersArgumentType MANY = new ComputersArgumentType(false);
 | 
			
		||||
    private static final ComputersArgumentType SOME = new ComputersArgumentType(true);
 | 
			
		||||
 | 
			
		||||
    private static final List<String> EXAMPLES = List.of(
 | 
			
		||||
        "0", "#0", "@Label", "~Advanced"
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    public static ComputersArgumentType manyComputers() {
 | 
			
		||||
        return MANY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static ComputersArgumentType someComputers() {
 | 
			
		||||
        return SOME;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static Collection<ServerComputer> getComputersArgument(CommandContext<CommandSourceStack> context, String name) throws CommandSyntaxException {
 | 
			
		||||
        return context.getArgument(name, ComputersSupplier.class).unwrap(context.getSource());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private final boolean requireSome;
 | 
			
		||||
 | 
			
		||||
    private ComputersArgumentType(boolean requireSome) {
 | 
			
		||||
        this.requireSome = requireSome;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public ComputersSupplier parse(StringReader reader) throws CommandSyntaxException {
 | 
			
		||||
        var start = reader.getCursor();
 | 
			
		||||
        var kind = reader.peek();
 | 
			
		||||
        ComputersSupplier computers;
 | 
			
		||||
        if (kind == '@') {
 | 
			
		||||
            reader.skip();
 | 
			
		||||
            var label = reader.readUnquotedString();
 | 
			
		||||
            computers = getComputers(x -> Objects.equals(label, x.getLabel()));
 | 
			
		||||
        } else if (kind == '~') {
 | 
			
		||||
            reader.skip();
 | 
			
		||||
            var family = reader.readUnquotedString();
 | 
			
		||||
            computers = getComputers(x -> x.getFamily().name().equalsIgnoreCase(family));
 | 
			
		||||
        } else if (kind == '#') {
 | 
			
		||||
            reader.skip();
 | 
			
		||||
            var id = reader.readInt();
 | 
			
		||||
            computers = getComputers(x -> x.getID() == id);
 | 
			
		||||
        } else {
 | 
			
		||||
            var instance = reader.readInt();
 | 
			
		||||
            computers = s -> {
 | 
			
		||||
                var computer = ServerContext.get(s.getServer()).registry().get(instance);
 | 
			
		||||
                return computer == null ? List.of() : List.of(computer);
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (requireSome) {
 | 
			
		||||
            var selector = reader.getString().substring(start, reader.getCursor());
 | 
			
		||||
            return source -> {
 | 
			
		||||
                var matched = computers.unwrap(source);
 | 
			
		||||
                if (matched.isEmpty()) throw COMPUTER_ARG_NONE.create(selector);
 | 
			
		||||
                return matched;
 | 
			
		||||
            };
 | 
			
		||||
        } else {
 | 
			
		||||
            return computers;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
 | 
			
		||||
        var remaining = builder.getRemaining();
 | 
			
		||||
 | 
			
		||||
        // We can run this one on the client, for obvious reasons.
 | 
			
		||||
        if (remaining.startsWith("~")) {
 | 
			
		||||
            return suggest(builder, ComputerFamily.values(), x -> "~" + x.name());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Verify we've a command source and we're running on the server
 | 
			
		||||
        return suggestOnServer(context, s -> {
 | 
			
		||||
            if (remaining.startsWith("@")) {
 | 
			
		||||
                suggestComputers(s.getSource(), builder, remaining, x -> {
 | 
			
		||||
                    var label = x.getLabel();
 | 
			
		||||
                    return label == null ? null : "@" + label;
 | 
			
		||||
                });
 | 
			
		||||
            } else if (remaining.startsWith("#")) {
 | 
			
		||||
                suggestComputers(s.getSource(), builder, remaining, c -> "#" + c.getID());
 | 
			
		||||
            } else {
 | 
			
		||||
                suggestComputers(s.getSource(), builder, remaining, c -> Integer.toString(c.getInstanceID()));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return builder.buildFuture();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Collection<String> getExamples() {
 | 
			
		||||
        return EXAMPLES;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void suggestComputers(CommandSourceStack source, SuggestionsBuilder builder, String remaining, Function<ServerComputer, String> renderer) {
 | 
			
		||||
        remaining = remaining.toLowerCase(Locale.ROOT);
 | 
			
		||||
        for (var computer : ServerContext.get(source.getServer()).registry().getComputers()) {
 | 
			
		||||
            var converted = renderer.apply(computer);
 | 
			
		||||
            if (converted != null && converted.toLowerCase(Locale.ROOT).startsWith(remaining)) {
 | 
			
		||||
                builder.suggest(converted);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static ComputersSupplier getComputers(Predicate<ServerComputer> predicate) {
 | 
			
		||||
        return s -> ServerContext.get(s.getServer()).registry()
 | 
			
		||||
            .getComputers()
 | 
			
		||||
            .stream()
 | 
			
		||||
            .filter(predicate)
 | 
			
		||||
            .toList();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static class Info implements ArgumentTypeInfo<ComputersArgumentType, Template> {
 | 
			
		||||
        @Override
 | 
			
		||||
        public void serializeToNetwork(ComputersArgumentType.Template arg, FriendlyByteBuf buf) {
 | 
			
		||||
            buf.writeBoolean(arg.requireSome());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public ComputersArgumentType.Template deserializeFromNetwork(FriendlyByteBuf buf) {
 | 
			
		||||
            var requiresSome = buf.readBoolean();
 | 
			
		||||
            return new ComputersArgumentType.Template(this, requiresSome);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void serializeToJson(ComputersArgumentType.Template arg, JsonObject json) {
 | 
			
		||||
            json.addProperty("requireSome", arg.requireSome);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public ComputersArgumentType.Template unpack(ComputersArgumentType argumentType) {
 | 
			
		||||
            return new ComputersArgumentType.Template(this, argumentType.requireSome);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public record Template(Info info, boolean requireSome) implements ArgumentTypeInfo.Template<ComputersArgumentType> {
 | 
			
		||||
        @Override
 | 
			
		||||
        public ComputersArgumentType instantiate(CommandBuildContext context) {
 | 
			
		||||
            return requireSome ? SOME : MANY;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public Info type() {
 | 
			
		||||
            return info;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @FunctionalInterface
 | 
			
		||||
    public interface ComputersSupplier {
 | 
			
		||||
        Collection<ServerComputer> unwrap(CommandSourceStack source) throws CommandSyntaxException;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static Set<ServerComputer> unwrap(CommandSourceStack source, Collection<ComputersSupplier> suppliers) throws CommandSyntaxException {
 | 
			
		||||
        Set<ServerComputer> computers = new HashSet<>();
 | 
			
		||||
        for (var supplier : suppliers) computers.addAll(supplier.unwrap(source));
 | 
			
		||||
        return computers;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,6 +4,8 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.command.text;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ServerComputer;
 | 
			
		||||
import dan200.computercraft.shared.platform.PlatformHelper;
 | 
			
		||||
import net.minecraft.ChatFormatting;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.network.chat.ClickEvent;
 | 
			
		||||
@@ -53,6 +55,13 @@ public final class ChatHelpers {
 | 
			
		||||
        return link(component, new ClickEvent(ClickEvent.Action.RUN_COMMAND, command), toolTip);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static Component clientLink(MutableComponent component, String command, Component toolTip) {
 | 
			
		||||
        var event = PlatformHelper.get().canClickRunClientCommand()
 | 
			
		||||
            ? new ClickEvent(ClickEvent.Action.RUN_COMMAND, command)
 | 
			
		||||
            : new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, command);
 | 
			
		||||
        return link(component, event, toolTip);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static Component link(Component component, ClickEvent click, Component toolTip) {
 | 
			
		||||
        var style = component.getStyle();
 | 
			
		||||
 | 
			
		||||
@@ -73,4 +82,16 @@ public final class ChatHelpers {
 | 
			
		||||
            .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.translatable("gui.computercraft.tooltip.copy")))
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static String makeComputerCommand(String command, ServerComputer computer) {
 | 
			
		||||
        return String.format("/computercraft %s @c[instance=%s]", command, computer.getInstanceUUID());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static Component makeComputerDumpCommand(ServerComputer computer) {
 | 
			
		||||
        return link(
 | 
			
		||||
            text("#" + computer.getID()),
 | 
			
		||||
            makeComputerCommand("dump", computer),
 | 
			
		||||
            Component.translatable("commands.computercraft.dump.action")
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ import net.minecraft.world.entity.player.Player;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.level.BlockGetter;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
import net.minecraft.world.level.LevelAccessor;
 | 
			
		||||
import net.minecraft.world.level.LevelReader;
 | 
			
		||||
import net.minecraft.world.level.block.Block;
 | 
			
		||||
import net.minecraft.world.level.block.EntityBlock;
 | 
			
		||||
@@ -176,6 +177,15 @@ public abstract class AbstractComputerBlock<T extends AbstractComputerBlockEntit
 | 
			
		||||
        if (be instanceof AbstractComputerBlockEntity computer) computer.neighborChanged(neighbour);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    @Deprecated
 | 
			
		||||
    public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor level, BlockPos pos, BlockPos neighborPos) {
 | 
			
		||||
        var be = level.getBlockEntity(pos);
 | 
			
		||||
        if (be instanceof AbstractComputerBlockEntity computer) computer.neighbourShapeChanged(direction);
 | 
			
		||||
 | 
			
		||||
        return super.updateShape(state, direction, neighborState, level, pos, neighborPos);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    @Override
 | 
			
		||||
    @Deprecated
 | 
			
		||||
 
 | 
			
		||||
@@ -36,13 +36,14 @@ import net.minecraft.world.level.block.state.BlockState;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
 | 
			
		||||
public abstract class AbstractComputerBlockEntity extends BlockEntity implements IComputerBlockEntity, Nameable, MenuProvider {
 | 
			
		||||
    private static final String NBT_ID = "ComputerId";
 | 
			
		||||
    private static final String NBT_LABEL = "Label";
 | 
			
		||||
    private static final String NBT_ON = "On";
 | 
			
		||||
 | 
			
		||||
    private int instanceID = -1;
 | 
			
		||||
    private @Nullable UUID instanceID = null;
 | 
			
		||||
    private int computerID = -1;
 | 
			
		||||
    protected @Nullable String label = null;
 | 
			
		||||
    private boolean on = false;
 | 
			
		||||
@@ -66,7 +67,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
 | 
			
		||||
 | 
			
		||||
        var computer = getServerComputer();
 | 
			
		||||
        if (computer != null) computer.close();
 | 
			
		||||
        instanceID = -1;
 | 
			
		||||
        instanceID = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -113,19 +114,16 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
 | 
			
		||||
        return InteractionResult.PASS;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void neighborChanged(BlockPos neighbour) {
 | 
			
		||||
        updateInputAt(neighbour);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected void serverTick() {
 | 
			
		||||
        if (getLevel().isClientSide) return;
 | 
			
		||||
        if (computerID < 0 && !startOn) return; // Don't tick if we don't need a computer!
 | 
			
		||||
 | 
			
		||||
        var computer = createServerComputer();
 | 
			
		||||
 | 
			
		||||
        // Update any peripherals that have changed.
 | 
			
		||||
        if (invalidSides != 0) {
 | 
			
		||||
            for (var direction : DirectionUtil.FACINGS) {
 | 
			
		||||
                if ((invalidSides & (1 << direction.ordinal())) != 0) refreshPeripheral(computer, direction);
 | 
			
		||||
                if (DirectionUtil.isSet(invalidSides, direction)) refreshPeripheral(computer, direction);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -139,16 +137,30 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
 | 
			
		||||
 | 
			
		||||
        fresh = false;
 | 
			
		||||
        computerID = computer.getID();
 | 
			
		||||
        label = computer.getLabel();
 | 
			
		||||
        on = computer.isOn();
 | 
			
		||||
 | 
			
		||||
        // Update the block state if needed. We don't fire a block update intentionally,
 | 
			
		||||
        // as this only really is needed on the client side.
 | 
			
		||||
        // If the on state has changed, mark as as dirty.
 | 
			
		||||
        var newOn = computer.isOn();
 | 
			
		||||
        if (on != newOn) {
 | 
			
		||||
            on = newOn;
 | 
			
		||||
            setChanged();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If the label has changed, mark as dirty and sync to client.
 | 
			
		||||
        var newLabel = computer.getLabel();
 | 
			
		||||
        if (!Objects.equals(label, newLabel)) {
 | 
			
		||||
            label = newLabel;
 | 
			
		||||
            BlockEntityHelpers.updateBlock(this);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Update the block state if needed.
 | 
			
		||||
        updateBlockState(computer.getState());
 | 
			
		||||
 | 
			
		||||
        // TODO: This should ideally be split up into label/id/on (which should save NBT and sync to client) and
 | 
			
		||||
        //  redstone (which should update outputs)
 | 
			
		||||
        if (computer.hasOutputChanged()) updateOutput();
 | 
			
		||||
        var changes = computer.pollAndResetChanges();
 | 
			
		||||
        if (changes != 0) {
 | 
			
		||||
            for (var direction : DirectionUtil.FACINGS) {
 | 
			
		||||
                if ((changes & (1 << remapToLocalSide(direction).ordinal())) != 0) updateRedstoneTo(direction);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected abstract void updateBlockState(ComputerState newState);
 | 
			
		||||
@@ -198,11 +210,15 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
 | 
			
		||||
        return localSide;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void updateRedstoneInputs(ServerComputer computer) {
 | 
			
		||||
        var pos = getBlockPos();
 | 
			
		||||
        for (var dir : DirectionUtil.FACINGS) updateRedstoneInput(computer, dir, pos.relative(dir));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the redstone input on a particular side.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * This is called <em>immediately</em> when a neighbouring block changes (see {@link #neighborChanged(BlockPos)}).
 | 
			
		||||
     *
 | 
			
		||||
     * @param computer  The current server computer.
 | 
			
		||||
     * @param dir       The direction to update in.
 | 
			
		||||
     * @param targetPos The position of the adjacent block, equal to {@code getBlockPos().offset(dir)}.
 | 
			
		||||
     */
 | 
			
		||||
    private void updateRedstoneInput(ServerComputer computer, Direction dir, BlockPos targetPos) {
 | 
			
		||||
        var offsetSide = dir.getOpposite();
 | 
			
		||||
        var localDir = remapToLocalSide(dir);
 | 
			
		||||
@@ -211,6 +227,15 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
 | 
			
		||||
        computer.setBundledRedstoneInput(localDir, BundledRedstone.getOutput(getLevel(), targetPos, offsetSide));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the peripheral on a particular side.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * This is called from {@link #serverTick()}, after a peripheral has been marked as invalid (such as in
 | 
			
		||||
     * {@link #neighborChanged(BlockPos)})
 | 
			
		||||
     *
 | 
			
		||||
     * @param computer The current server computer.
 | 
			
		||||
     * @param dir      The direction to update in.
 | 
			
		||||
     */
 | 
			
		||||
    private void refreshPeripheral(ServerComputer computer, Direction dir) {
 | 
			
		||||
        invalidSides &= ~(1 << dir.ordinal());
 | 
			
		||||
 | 
			
		||||
@@ -243,7 +268,18 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void updateInputAt(BlockPos neighbour) {
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when a neighbour block changes.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * This finds the side the neighbour block is on, and updates the inputs accordingly.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * We do <strong>NOT</strong> update the peripheral immediately. Blocks and block entities are sometimes
 | 
			
		||||
     * inconsistent at the point where an update is received, and so we instead just mark that side as dirty (see
 | 
			
		||||
     * {@link #invalidSides}) and refresh it {@linkplain #serverTick() next tick}.
 | 
			
		||||
     *
 | 
			
		||||
     * @param neighbour The position of the neighbour block.
 | 
			
		||||
     */
 | 
			
		||||
    public void neighborChanged(BlockPos neighbour) {
 | 
			
		||||
        var computer = getServerComputer();
 | 
			
		||||
        if (computer == null) return;
 | 
			
		||||
 | 
			
		||||
@@ -258,22 +294,39 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
 | 
			
		||||
 | 
			
		||||
        // If the position is not any adjacent one, update all inputs. This is pretty terrible, but some redstone mods
 | 
			
		||||
        // handle this incorrectly.
 | 
			
		||||
        updateRedstoneInputs(computer);
 | 
			
		||||
        invalidSides = (1 << 6) - 1; // Mark all peripherals as dirty.
 | 
			
		||||
        for (var dir : DirectionUtil.FACINGS) updateRedstoneInput(computer, dir, getBlockPos().relative(dir));
 | 
			
		||||
        invalidSides = DirectionUtil.ALL_SIDES; // Mark all peripherals as dirty.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the block's state and propagate redstone output.
 | 
			
		||||
     * Called when a neighbour block's shape changes.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * Unlike {@link #neighborChanged(BlockPos)}, we don't update redstone, only peripherals.
 | 
			
		||||
     *
 | 
			
		||||
     * @param direction The side that changed.
 | 
			
		||||
     */
 | 
			
		||||
    public void updateOutput() {
 | 
			
		||||
        BlockEntityHelpers.updateBlock(this);
 | 
			
		||||
        for (var dir : DirectionUtil.FACINGS) RedstoneUtil.propagateRedstoneOutput(getLevel(), getBlockPos(), dir);
 | 
			
		||||
 | 
			
		||||
        var computer = getServerComputer();
 | 
			
		||||
        if (computer != null) updateRedstoneInputs(computer);
 | 
			
		||||
    public void neighbourShapeChanged(Direction direction) {
 | 
			
		||||
        invalidSides |= 1 << direction.ordinal();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected abstract ServerComputer createComputer(int id);
 | 
			
		||||
    /**
 | 
			
		||||
     * Update outputs in a specific direction.
 | 
			
		||||
     *
 | 
			
		||||
     * @param direction The direction to propagate outputs in.
 | 
			
		||||
     */
 | 
			
		||||
    protected void updateRedstoneTo(Direction direction) {
 | 
			
		||||
        RedstoneUtil.propagateRedstoneOutput(getLevel(), getBlockPos(), direction);
 | 
			
		||||
 | 
			
		||||
        var computer = getServerComputer();
 | 
			
		||||
        if (computer != null) updateRedstoneInput(computer, direction, getBlockPos().relative(direction));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update all redstone outputs.
 | 
			
		||||
     */
 | 
			
		||||
    public void updateRedstone() {
 | 
			
		||||
        for (var dir : DirectionUtil.FACINGS) updateRedstoneTo(dir);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final int getComputerID() {
 | 
			
		||||
@@ -331,6 +384,8 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
 | 
			
		||||
        return computer;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected abstract ServerComputer createComputer(int id);
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public ServerComputer getServerComputer() {
 | 
			
		||||
        return getLevel().isClientSide || getLevel().getServer() == null ? null : ServerContext.get(getLevel().getServer()).registry().get(instanceID);
 | 
			
		||||
@@ -358,7 +413,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected void transferStateFrom(AbstractComputerBlockEntity copy) {
 | 
			
		||||
        if (copy.computerID != computerID || copy.instanceID != instanceID) {
 | 
			
		||||
        if (copy.computerID != computerID || !Objects.equals(copy.instanceID, instanceID)) {
 | 
			
		||||
            unload();
 | 
			
		||||
            instanceID = copy.instanceID;
 | 
			
		||||
            computerID = copy.computerID;
 | 
			
		||||
@@ -368,7 +423,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
 | 
			
		||||
            lockCode = copy.lockCode;
 | 
			
		||||
            BlockEntityHelpers.updateBlock(this);
 | 
			
		||||
        }
 | 
			
		||||
        copy.instanceID = -1;
 | 
			
		||||
        copy.instanceID = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,7 @@ public class ComputerBlockEntity extends AbstractComputerBlockEntity {
 | 
			
		||||
    protected void updateBlockState(ComputerState newState) {
 | 
			
		||||
        var existing = getBlockState();
 | 
			
		||||
        if (existing.getValue(ComputerBlock.STATE) != newState) {
 | 
			
		||||
            getLevel().setBlock(getBlockPos(), existing.setValue(ComputerBlock.STATE, newState), 3);
 | 
			
		||||
            getLevel().setBlock(getBlockPos(), existing.setValue(ComputerBlock.STATE, newState), ComputerBlock.UPDATE_CLIENTS);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,11 +26,13 @@ import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
import net.minecraft.world.inventory.AbstractContainerMenu;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
import java.util.concurrent.atomic.AtomicBoolean;
 | 
			
		||||
import java.util.function.Function;
 | 
			
		||||
 | 
			
		||||
public class ServerComputer implements InputHandler, ComputerEnvironment {
 | 
			
		||||
    private final int instanceID;
 | 
			
		||||
    private final UUID instanceUUID = UUID.randomUUID();
 | 
			
		||||
 | 
			
		||||
    private ServerLevel level;
 | 
			
		||||
    private BlockPos position;
 | 
			
		||||
@@ -42,7 +44,6 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
 | 
			
		||||
    private final NetworkedTerminal terminal;
 | 
			
		||||
    private final AtomicBoolean terminalChanged = new AtomicBoolean(false);
 | 
			
		||||
 | 
			
		||||
    private boolean changedLastFrame;
 | 
			
		||||
    private int ticksSincePing;
 | 
			
		||||
 | 
			
		||||
    public ServerComputer(
 | 
			
		||||
@@ -96,10 +97,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
 | 
			
		||||
 | 
			
		||||
    public void tickServer() {
 | 
			
		||||
        ticksSincePing++;
 | 
			
		||||
 | 
			
		||||
        computer.tick();
 | 
			
		||||
 | 
			
		||||
        changedLastFrame = computer.pollAndResetChanged();
 | 
			
		||||
        if (terminalChanged.getAndSet(false)) onTerminalChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -119,13 +117,13 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
 | 
			
		||||
        return ticksSincePing > 100;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean hasOutputChanged() {
 | 
			
		||||
        return changedLastFrame;
 | 
			
		||||
    public int pollAndResetChanges() {
 | 
			
		||||
        return computer.pollAndResetChanges();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public int register() {
 | 
			
		||||
        ServerContext.get(level.getServer()).registry().add(instanceID, this);
 | 
			
		||||
        return instanceID;
 | 
			
		||||
    public UUID register() {
 | 
			
		||||
        ServerContext.get(level.getServer()).registry().add(this);
 | 
			
		||||
        return instanceUUID;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void unload() {
 | 
			
		||||
@@ -134,7 +132,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
 | 
			
		||||
 | 
			
		||||
    public void close() {
 | 
			
		||||
        unload();
 | 
			
		||||
        ServerContext.get(level.getServer()).registry().remove(instanceID);
 | 
			
		||||
        ServerContext.get(level.getServer()).registry().remove(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void sendToAllInteracting(Function<AbstractContainerMenu, NetworkMessage<ClientNetworkContext>> createPacket) {
 | 
			
		||||
@@ -154,6 +152,10 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
 | 
			
		||||
        return instanceID;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public UUID getInstanceUUID() {
 | 
			
		||||
        return instanceUUID;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public int getID() {
 | 
			
		||||
        return computer.getID();
 | 
			
		||||
    }
 | 
			
		||||
@@ -167,7 +169,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ComputerState getState() {
 | 
			
		||||
        if (!isOn()) return ComputerState.OFF;
 | 
			
		||||
        if (!computer.isOn()) return ComputerState.OFF;
 | 
			
		||||
        return computer.isBlinking() ? ComputerState.BLINKING : ComputerState.ON;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,14 +8,14 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 | 
			
		||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.Random;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
 | 
			
		||||
public class ServerComputerRegistry {
 | 
			
		||||
    private static final Random RANDOM = new Random();
 | 
			
		||||
 | 
			
		||||
    private final int sessionId = RANDOM.nextInt();
 | 
			
		||||
    private final Int2ObjectMap<ServerComputer> computers = new Int2ObjectOpenHashMap<>();
 | 
			
		||||
    private final Int2ObjectMap<ServerComputer> computersByInstanceId = new Int2ObjectOpenHashMap<>();
 | 
			
		||||
    private final Map<UUID, ServerComputer> computersByInstanceUuid = new HashMap<>();
 | 
			
		||||
    private int nextInstanceId;
 | 
			
		||||
 | 
			
		||||
    public int getSessionID() {
 | 
			
		||||
@@ -28,11 +28,16 @@ public class ServerComputerRegistry {
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public ServerComputer get(int instanceID) {
 | 
			
		||||
        return instanceID >= 0 ? computers.get(instanceID) : null;
 | 
			
		||||
        return instanceID >= 0 ? computersByInstanceId.get(instanceID) : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public ServerComputer get(int sessionId, int instanceId) {
 | 
			
		||||
    public ServerComputer get(@Nullable UUID instanceID) {
 | 
			
		||||
        return instanceID != null ? computersByInstanceUuid.get(instanceID) : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public ServerComputer get(int sessionId, @Nullable UUID instanceId) {
 | 
			
		||||
        return sessionId == this.sessionId ? get(instanceId) : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -50,28 +55,36 @@ public class ServerComputerRegistry {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void add(int instanceID, ServerComputer computer) {
 | 
			
		||||
        remove(instanceID);
 | 
			
		||||
        computers.put(instanceID, computer);
 | 
			
		||||
        nextInstanceId = Math.max(nextInstanceId, instanceID + 1);
 | 
			
		||||
    void add(ServerComputer computer) {
 | 
			
		||||
        var instanceID = computer.getInstanceID();
 | 
			
		||||
        var instanceUUID = computer.getInstanceUUID();
 | 
			
		||||
 | 
			
		||||
        if (computersByInstanceId.containsKey(instanceID)) {
 | 
			
		||||
            throw new IllegalStateException("Duplicate computer " + instanceID);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    void remove(int instanceID) {
 | 
			
		||||
        var computer = get(instanceID);
 | 
			
		||||
        if (computer != null) {
 | 
			
		||||
        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();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        computers.remove(instanceID);
 | 
			
		||||
        computersByInstanceId.remove(computer.getInstanceID());
 | 
			
		||||
        computersByInstanceUuid.remove(computer.getInstanceUUID());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void close() {
 | 
			
		||||
        for (var computer : getComputers()) computer.unload();
 | 
			
		||||
        computers.clear();
 | 
			
		||||
        computersByInstanceId.clear();
 | 
			
		||||
        computersByInstanceUuid.clear();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Collection<ServerComputer> getComputers() {
 | 
			
		||||
        return computers.values();
 | 
			
		||||
        return computersByInstanceId.values();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ public class ViewComputerMenu extends ComputerMenuWithoutInventory {
 | 
			
		||||
 | 
			
		||||
    private static boolean canInteractWith(ServerComputer computer, Player player) {
 | 
			
		||||
        // If this computer no longer exists then discard it.
 | 
			
		||||
        if (ServerContext.get(computer.getLevel().getServer()).registry().get(computer.getInstanceID()) != computer) {
 | 
			
		||||
        if (ServerContext.get(computer.getLevel().getServer()).registry().get(computer.getInstanceUUID()) != computer) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import dan200.computercraft.shared.command.text.TableBuilder;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerState;
 | 
			
		||||
import dan200.computercraft.shared.computer.terminal.TerminalState;
 | 
			
		||||
import dan200.computercraft.shared.computer.upload.UploadResult;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
@@ -15,7 +16,6 @@ import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.sounds.SoundEvent;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -30,11 +30,11 @@ public interface ClientNetworkContext {
 | 
			
		||||
 | 
			
		||||
    void handlePlayRecord(BlockPos pos, @Nullable SoundEvent sound, @Nullable String name);
 | 
			
		||||
 | 
			
		||||
    void handlePocketComputerData(int instanceId, ComputerState state, int lightState, TerminalState terminal);
 | 
			
		||||
    void handlePocketComputerData(UUID instanceId, ComputerState state, int lightState, TerminalState terminal);
 | 
			
		||||
 | 
			
		||||
    void handlePocketComputerDeleted(int instanceId);
 | 
			
		||||
    void handlePocketComputerDeleted(UUID instanceId);
 | 
			
		||||
 | 
			
		||||
    void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, ByteBuffer audio);
 | 
			
		||||
    void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, EncodedAudio audio);
 | 
			
		||||
 | 
			
		||||
    void handleSpeakerMove(UUID source, SpeakerPosition.Message position);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,24 +13,26 @@ import dan200.computercraft.shared.network.NetworkMessages;
 | 
			
		||||
import dan200.computercraft.shared.pocket.core.PocketServerComputer;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provides additional data about a client computer, such as its ID and current state.
 | 
			
		||||
 */
 | 
			
		||||
public class PocketComputerDataMessage implements NetworkMessage<ClientNetworkContext> {
 | 
			
		||||
    private final int instanceId;
 | 
			
		||||
    private final UUID clientId;
 | 
			
		||||
    private final ComputerState state;
 | 
			
		||||
    private final int lightState;
 | 
			
		||||
    private final TerminalState terminal;
 | 
			
		||||
 | 
			
		||||
    public PocketComputerDataMessage(PocketServerComputer computer, boolean sendTerminal) {
 | 
			
		||||
        instanceId = computer.getInstanceID();
 | 
			
		||||
        clientId = computer.getInstanceUUID();
 | 
			
		||||
        state = computer.getState();
 | 
			
		||||
        lightState = computer.getLight();
 | 
			
		||||
        terminal = sendTerminal ? computer.getTerminalState() : new TerminalState((NetworkedTerminal) null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public PocketComputerDataMessage(FriendlyByteBuf buf) {
 | 
			
		||||
        instanceId = buf.readVarInt();
 | 
			
		||||
        clientId = buf.readUUID();
 | 
			
		||||
        state = buf.readEnum(ComputerState.class);
 | 
			
		||||
        lightState = buf.readVarInt();
 | 
			
		||||
        terminal = new TerminalState(buf);
 | 
			
		||||
@@ -38,7 +40,7 @@ public class PocketComputerDataMessage implements NetworkMessage<ClientNetworkCo
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void write(FriendlyByteBuf buf) {
 | 
			
		||||
        buf.writeVarInt(instanceId);
 | 
			
		||||
        buf.writeUUID(clientId);
 | 
			
		||||
        buf.writeEnum(state);
 | 
			
		||||
        buf.writeVarInt(lightState);
 | 
			
		||||
        terminal.write(buf);
 | 
			
		||||
@@ -46,7 +48,7 @@ public class PocketComputerDataMessage implements NetworkMessage<ClientNetworkCo
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void handle(ClientNetworkContext context) {
 | 
			
		||||
        context.handlePocketComputerData(instanceId, state, lightState, terminal);
 | 
			
		||||
        context.handlePocketComputerData(clientId, state, lightState, terminal);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
 
 | 
			
		||||
@@ -9,21 +9,23 @@ import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessages;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
public class PocketComputerDeletedClientMessage implements NetworkMessage<ClientNetworkContext> {
 | 
			
		||||
    private final int instanceId;
 | 
			
		||||
    private final UUID instanceId;
 | 
			
		||||
 | 
			
		||||
    public PocketComputerDeletedClientMessage(int instanceId) {
 | 
			
		||||
    public PocketComputerDeletedClientMessage(UUID instanceId) {
 | 
			
		||||
        this.instanceId = instanceId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public PocketComputerDeletedClientMessage(FriendlyByteBuf buffer) {
 | 
			
		||||
        instanceId = buffer.readVarInt();
 | 
			
		||||
        instanceId = buffer.readUUID();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void write(FriendlyByteBuf buf) {
 | 
			
		||||
        buf.writeVarInt(instanceId);
 | 
			
		||||
        buf.writeUUID(instanceId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
 
 | 
			
		||||
@@ -7,11 +7,11 @@ package dan200.computercraft.shared.network.client;
 | 
			
		||||
import dan200.computercraft.shared.network.MessageType;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessages;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerBlockEntity;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -24,10 +24,10 @@ import java.util.UUID;
 | 
			
		||||
public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkContext> {
 | 
			
		||||
    private final UUID source;
 | 
			
		||||
    private final SpeakerPosition.Message pos;
 | 
			
		||||
    private final ByteBuffer content;
 | 
			
		||||
    private final EncodedAudio content;
 | 
			
		||||
    private final float volume;
 | 
			
		||||
 | 
			
		||||
    public SpeakerAudioClientMessage(UUID source, SpeakerPosition pos, float volume, ByteBuffer content) {
 | 
			
		||||
    public SpeakerAudioClientMessage(UUID source, SpeakerPosition pos, float volume, EncodedAudio content) {
 | 
			
		||||
        this.source = source;
 | 
			
		||||
        this.pos = pos.asMessage();
 | 
			
		||||
        this.content = content;
 | 
			
		||||
@@ -38,10 +38,7 @@ public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkCo
 | 
			
		||||
        source = buf.readUUID();
 | 
			
		||||
        pos = SpeakerPosition.Message.read(buf);
 | 
			
		||||
        volume = buf.readFloat();
 | 
			
		||||
 | 
			
		||||
        var bytes = new byte[buf.readableBytes()];
 | 
			
		||||
        buf.readBytes(bytes);
 | 
			
		||||
        content = ByteBuffer.wrap(bytes);
 | 
			
		||||
        content = EncodedAudio.read(buf);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -49,7 +46,7 @@ public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkCo
 | 
			
		||||
        buf.writeUUID(source);
 | 
			
		||||
        pos.write(buf);
 | 
			
		||||
        buf.writeFloat(volume);
 | 
			
		||||
        buf.writeBytes(content.duplicate());
 | 
			
		||||
        content.write(buf);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
 
 | 
			
		||||
@@ -11,9 +11,8 @@ import dan200.computercraft.api.lua.MethodResult;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IComputerAccess;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IDynamicPeripheral;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import dan200.computercraft.impl.RegistryHelper;
 | 
			
		||||
import dan200.computercraft.core.computer.GuardedLuaContext;
 | 
			
		||||
import net.minecraft.core.Direction;
 | 
			
		||||
import net.minecraft.core.registries.BuiltInRegistries;
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntity;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
@@ -28,13 +27,16 @@ public final class GenericPeripheral implements IDynamicPeripheral {
 | 
			
		||||
    private final Set<String> additionalTypes;
 | 
			
		||||
    private final List<SaturatedMethod> methods;
 | 
			
		||||
 | 
			
		||||
    GenericPeripheral(BlockEntity tile, Direction side, @Nullable String name, Set<String> additionalTypes, List<SaturatedMethod> methods) {
 | 
			
		||||
    private @Nullable GuardedLuaContext contextWrapper;
 | 
			
		||||
    private final GuardedLuaContext.Guard guard;
 | 
			
		||||
 | 
			
		||||
    GenericPeripheral(BlockEntity tile, Direction side, String type, Set<String> additionalTypes, List<SaturatedMethod> methods) {
 | 
			
		||||
        this.side = side;
 | 
			
		||||
        var type = RegistryHelper.getKeyOrThrow(BuiltInRegistries.BLOCK_ENTITY_TYPE, tile.getType());
 | 
			
		||||
        this.tile = tile;
 | 
			
		||||
        this.type = name != null ? name : type.toString();
 | 
			
		||||
        this.type = type;
 | 
			
		||||
        this.additionalTypes = additionalTypes;
 | 
			
		||||
        this.methods = methods;
 | 
			
		||||
        this.guard = () -> !tile.isRemoved() && tile.getLevel() != null && tile.getLevel().isLoaded(tile.getBlockPos());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Direction side() {
 | 
			
		||||
@@ -50,7 +52,12 @@ public final class GenericPeripheral implements IDynamicPeripheral {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public MethodResult callMethod(IComputerAccess computer, ILuaContext context, int method, IArguments arguments) throws LuaException {
 | 
			
		||||
        return methods.get(method).apply(context, computer, arguments);
 | 
			
		||||
        var contextWrapper = this.contextWrapper;
 | 
			
		||||
        if (contextWrapper == null || !contextWrapper.wraps(context)) {
 | 
			
		||||
            contextWrapper = this.contextWrapper = new GuardedLuaContext(context, guard);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return methods.get(method).apply(contextWrapper, computer, arguments);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,9 @@ import dan200.computercraft.core.methods.NamedMethod;
 | 
			
		||||
import dan200.computercraft.core.methods.PeripheralMethod;
 | 
			
		||||
import net.minecraft.core.Direction;
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntity;
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntityType;
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
@@ -25,6 +28,8 @@ import java.util.Set;
 | 
			
		||||
 * See the platform-specific peripheral providers for the usage of this.
 | 
			
		||||
 */
 | 
			
		||||
final class GenericPeripheralBuilder {
 | 
			
		||||
    private static final Logger LOG = LoggerFactory.getLogger(GenericPeripheralBuilder.class);
 | 
			
		||||
 | 
			
		||||
    private @Nullable String name;
 | 
			
		||||
    private final Set<String> additionalTypes = new HashSet<>(0);
 | 
			
		||||
    private final ArrayList<SaturatedMethod> methods = new ArrayList<>();
 | 
			
		||||
@@ -33,8 +38,24 @@ final class GenericPeripheralBuilder {
 | 
			
		||||
    IPeripheral toPeripheral(BlockEntity blockEntity, Direction side) {
 | 
			
		||||
        if (methods.isEmpty()) return null;
 | 
			
		||||
 | 
			
		||||
        String type;
 | 
			
		||||
        if (name == null) {
 | 
			
		||||
            var typeId = BlockEntityType.getKey(blockEntity.getType());
 | 
			
		||||
            if (typeId == null) {
 | 
			
		||||
                LOG.error(
 | 
			
		||||
                    "Block entity {} for {} was not registered. Skipping creating a generic peripheral for it.",
 | 
			
		||||
                    blockEntity, blockEntity.getBlockState().getBlock()
 | 
			
		||||
                );
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            type = typeId.toString();
 | 
			
		||||
        } else {
 | 
			
		||||
            type = name;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        methods.trimToSize();
 | 
			
		||||
        return new GenericPeripheral(blockEntity, side, name, additionalTypes, methods);
 | 
			
		||||
        return new GenericPeripheral(blockEntity, side, type, additionalTypes, methods);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void addMethod(Object target, String name, PeripheralMethod method, @Nullable NamedMethod<PeripheralMethod> info) {
 | 
			
		||||
 
 | 
			
		||||
@@ -127,9 +127,8 @@ public class CableBlock extends Block implements SimpleWaterloggedBlock, EntityB
 | 
			
		||||
            item = new ItemStack(ModRegistry.Items.CABLE.get());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        world.setBlock(pos, correctConnections(world, pos, newState), 3);
 | 
			
		||||
        world.setBlockAndUpdate(pos, correctConnections(world, pos, newState));
 | 
			
		||||
 | 
			
		||||
        cable.modemChanged();
 | 
			
		||||
        cable.connectionsChanged();
 | 
			
		||||
        if (!world.isClientSide && !player.getAbilities().instabuild) {
 | 
			
		||||
            Block.popResource(world, pos, item);
 | 
			
		||||
@@ -162,10 +161,7 @@ public class CableBlock extends Block implements SimpleWaterloggedBlock, EntityB
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setPlacedBy(Level world, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) {
 | 
			
		||||
        var tile = world.getBlockEntity(pos);
 | 
			
		||||
        if (tile instanceof CableBlockEntity cable) {
 | 
			
		||||
            if (cable.hasCable()) cable.connectionsChanged();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (tile instanceof CableBlockEntity cable && cable.hasCable()) cable.connectionsChanged();
 | 
			
		||||
        super.setPlacedBy(world, pos, state, placer, stack);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -177,14 +173,37 @@ public class CableBlock extends Block implements SimpleWaterloggedBlock, EntityB
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    @Deprecated
 | 
			
		||||
    public BlockState updateShape(BlockState state, Direction side, BlockState otherState, LevelAccessor world, BlockPos pos, BlockPos otherPos) {
 | 
			
		||||
        WaterloggableHelpers.updateShape(state, world, pos);
 | 
			
		||||
    public BlockState updateShape(BlockState state, Direction side, BlockState otherState, LevelAccessor level, BlockPos pos, BlockPos otherPos) {
 | 
			
		||||
        WaterloggableHelpers.updateShape(state, level, pos);
 | 
			
		||||
 | 
			
		||||
        // Should never happen, but handle the case where we've no modem or cable.
 | 
			
		||||
        if (!state.getValue(CABLE) && state.getValue(MODEM) == CableModemVariant.None) {
 | 
			
		||||
            return getFluidState(state).createLegacyBlock();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return world instanceof Level level ? state.setValue(CONNECTIONS.get(side), doesConnectVisually(state, level, pos, side)) : state;
 | 
			
		||||
        // Pop our modem if needed.
 | 
			
		||||
        var dir = state.getValue(MODEM).getFacing();
 | 
			
		||||
        if (dir != null && dir.equals(side) && !canSupportCenter(level, otherPos, side.getOpposite())) {
 | 
			
		||||
            // If we've no cable, follow normal Minecraft logic and just remove the block.
 | 
			
		||||
            if (!state.getValue(CABLE)) return getFluidState(state).createLegacyBlock();
 | 
			
		||||
 | 
			
		||||
            // Otherwise remove the cable and drop the modem manually.
 | 
			
		||||
            state = state.setValue(CableBlock.MODEM, CableModemVariant.None);
 | 
			
		||||
            if (level instanceof Level actualLevel) {
 | 
			
		||||
                Block.popResource(actualLevel, pos, new ItemStack(ModRegistry.Items.WIRED_MODEM.get()));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (level.getBlockEntity(pos) instanceof CableBlockEntity cable) cable.scheduleConnectionsChanged();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var modem = state.getValue(MODEM);
 | 
			
		||||
        if (modem.getFacing() == side && modem.isPeripheralOn() && level.getBlockEntity(pos) instanceof CableBlockEntity cable) {
 | 
			
		||||
            cable.queueRefreshPeripheral();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return level instanceof Level actualLevel
 | 
			
		||||
            ? state.setValue(CONNECTIONS.get(side), doesConnectVisually(state, actualLevel, pos, side))
 | 
			
		||||
            : state;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -230,6 +249,7 @@ public class CableBlock extends Block implements SimpleWaterloggedBlock, EntityB
 | 
			
		||||
    @Override
 | 
			
		||||
    @Deprecated
 | 
			
		||||
    public final InteractionResult use(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) {
 | 
			
		||||
        if (player.isCrouching() || !player.mayBuild()) return InteractionResult.PASS;
 | 
			
		||||
        return world.getBlockEntity(pos) instanceof CableBlockEntity modem ? modem.use(player) : InteractionResult.PASS;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ package dan200.computercraft.shared.peripheral.modem.wired;
 | 
			
		||||
import dan200.computercraft.api.network.wired.WiredElement;
 | 
			
		||||
import dan200.computercraft.api.network.wired.WiredNode;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import dan200.computercraft.shared.command.text.ChatHelpers;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.modem.ModemState;
 | 
			
		||||
import dan200.computercraft.shared.platform.ComponentAccess;
 | 
			
		||||
@@ -20,21 +19,16 @@ import net.minecraft.nbt.CompoundTag;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.world.InteractionResult;
 | 
			
		||||
import net.minecraft.world.entity.player.Player;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
import net.minecraft.world.level.block.Block;
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntity;
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntityType;
 | 
			
		||||
import net.minecraft.world.level.block.state.BlockState;
 | 
			
		||||
import net.minecraft.world.phys.Vec3;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
 | 
			
		||||
public class CableBlockEntity extends BlockEntity {
 | 
			
		||||
    private static final String NBT_PERIPHERAL_ENABLED = "PeripheralAccess";
 | 
			
		||||
 | 
			
		||||
    private final class CableElement extends WiredModemElement {
 | 
			
		||||
        @Override
 | 
			
		||||
        public Level getLevel() {
 | 
			
		||||
@@ -57,33 +51,21 @@ public class CableBlockEntity extends BlockEntity {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private boolean invalidPeripheral;
 | 
			
		||||
    private boolean peripheralAccessAllowed;
 | 
			
		||||
    private boolean refreshPeripheral;
 | 
			
		||||
    private final WiredModemLocalPeripheral peripheral = new WiredModemLocalPeripheral(PlatformHelper.get().createPeripheralAccess(this, x -> queueRefreshPeripheral()));
 | 
			
		||||
 | 
			
		||||
    private boolean connectionsFormed = false;
 | 
			
		||||
    private boolean connectionsChanged = false;
 | 
			
		||||
    private boolean refreshConnections = false;
 | 
			
		||||
 | 
			
		||||
    private final WiredModemElement cable = new CableElement();
 | 
			
		||||
    private final WiredNode node = cable.getNode();
 | 
			
		||||
    private final TickScheduler.Token tickToken = new TickScheduler.Token(this);
 | 
			
		||||
    private final WiredModemPeripheral modem = new WiredModemPeripheral(
 | 
			
		||||
        new ModemState(() -> TickScheduler.schedule(tickToken)),
 | 
			
		||||
        cable
 | 
			
		||||
        new ModemState(() -> TickScheduler.schedule(tickToken)), cable, peripheral, this
 | 
			
		||||
    ) {
 | 
			
		||||
        @Override
 | 
			
		||||
        protected WiredModemLocalPeripheral getLocalPeripheral() {
 | 
			
		||||
            return peripheral;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public Vec3 getPosition() {
 | 
			
		||||
            return Vec3.atCenterOf(getBlockPos().relative(getDirection()));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public Object getTarget() {
 | 
			
		||||
            return CableBlockEntity.this;
 | 
			
		||||
            var dir = getModemDirection();
 | 
			
		||||
            return Vec3.atCenterOf(dir == null ? getBlockPos() : getBlockPos().relative(dir));
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@@ -93,93 +75,61 @@ public class CableBlockEntity extends BlockEntity {
 | 
			
		||||
        super(type, pos, state);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void onRemove() {
 | 
			
		||||
        if (level == null || !level.isClientSide) {
 | 
			
		||||
            node.remove();
 | 
			
		||||
            connectionsFormed = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setRemoved() {
 | 
			
		||||
        super.setRemoved();
 | 
			
		||||
        modem.removed();
 | 
			
		||||
        onRemove();
 | 
			
		||||
        if (level == null || !level.isClientSide) node.remove();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void clearRemoved() {
 | 
			
		||||
        super.clearRemoved();
 | 
			
		||||
        refreshConnections = refreshPeripheral = true;
 | 
			
		||||
        TickScheduler.schedule(tickToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    @Deprecated
 | 
			
		||||
    public void setBlockState(BlockState state) {
 | 
			
		||||
        var direction = getMaybeDirection();
 | 
			
		||||
        var direction = getModemDirection();
 | 
			
		||||
        var hasCable = hasCable();
 | 
			
		||||
        super.setBlockState(state);
 | 
			
		||||
 | 
			
		||||
        // We invalidate both the modem and element if the modem's direction is different.
 | 
			
		||||
        if (getMaybeDirection() != direction) PlatformHelper.get().invalidateComponent(this);
 | 
			
		||||
        // We invalidate both the modem and element if the modem direction or cable are different.
 | 
			
		||||
        if (hasCable() != hasCable || getModemDirection() != direction) PlatformHelper.get().invalidateComponent(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    private Direction getMaybeDirection() {
 | 
			
		||||
    private Direction getModemDirection() {
 | 
			
		||||
        return getBlockState().getValue(CableBlock.MODEM).getFacing();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Direction getDirection() {
 | 
			
		||||
        var direction = getMaybeDirection();
 | 
			
		||||
        return direction == null ? Direction.NORTH : direction;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void neighborChanged(BlockPos neighbour) {
 | 
			
		||||
        var dir = getDirection();
 | 
			
		||||
        if (neighbour.equals(getBlockPos().relative(dir)) && hasModem() && !getBlockState().canSurvive(getLevel(), getBlockPos())) {
 | 
			
		||||
            if (hasCable()) {
 | 
			
		||||
                // Drop the modem and convert to cable
 | 
			
		||||
                Block.popResource(getLevel(), getBlockPos(), new ItemStack(ModRegistry.Items.WIRED_MODEM.get()));
 | 
			
		||||
                getLevel().setBlockAndUpdate(getBlockPos(), getBlockState().setValue(CableBlock.MODEM, CableModemVariant.None));
 | 
			
		||||
                modemChanged();
 | 
			
		||||
                connectionsChanged();
 | 
			
		||||
            } else {
 | 
			
		||||
                // Drop everything and remove block
 | 
			
		||||
                Block.popResource(getLevel(), getBlockPos(), new ItemStack(ModRegistry.Items.WIRED_MODEM.get()));
 | 
			
		||||
                getLevel().removeBlock(getBlockPos(), false);
 | 
			
		||||
                // This'll call #destroy(), so we don't need to reset the network here.
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!level.isClientSide && peripheralAccessAllowed) {
 | 
			
		||||
            var facing = getDirection();
 | 
			
		||||
            if (getBlockPos().relative(facing).equals(neighbour)) queueRefreshPeripheral();
 | 
			
		||||
        var dir = getModemDirection();
 | 
			
		||||
        if (!level.isClientSide && dir != null && getBlockPos().relative(dir).equals(neighbour) && isPeripheralOn()) {
 | 
			
		||||
            queueRefreshPeripheral();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void queueRefreshPeripheral() {
 | 
			
		||||
        if (invalidPeripheral) return;
 | 
			
		||||
        invalidPeripheral = true;
 | 
			
		||||
    void queueRefreshPeripheral() {
 | 
			
		||||
        refreshPeripheral = true;
 | 
			
		||||
        TickScheduler.schedule(tickToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void refreshPeripheral() {
 | 
			
		||||
        invalidPeripheral = false;
 | 
			
		||||
        if (level != null && !isRemoved() && peripheral.attach(level, getBlockPos(), getDirection())) {
 | 
			
		||||
            updateConnectedPeripherals();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    InteractionResult use(Player player) {
 | 
			
		||||
        if (player.isCrouching() || !player.mayBuild()) return InteractionResult.PASS;
 | 
			
		||||
        if (!canAttachPeripheral()) return InteractionResult.FAIL;
 | 
			
		||||
 | 
			
		||||
        if (getLevel().isClientSide) return InteractionResult.SUCCESS;
 | 
			
		||||
 | 
			
		||||
        var oldName = peripheral.getConnectedName();
 | 
			
		||||
        togglePeripheralAccess();
 | 
			
		||||
        if (isPeripheralOn()) {
 | 
			
		||||
            detachPeripheral();
 | 
			
		||||
        } else {
 | 
			
		||||
            attachPeripheral();
 | 
			
		||||
        }
 | 
			
		||||
        var newName = peripheral.getConnectedName();
 | 
			
		||||
 | 
			
		||||
        if (!Objects.equals(newName, oldName)) {
 | 
			
		||||
            if (oldName != null) {
 | 
			
		||||
                player.displayClientMessage(Component.translatable("chat.computercraft.wired_modem.peripheral_disconnected",
 | 
			
		||||
@@ -197,14 +147,11 @@ public class CableBlockEntity extends BlockEntity {
 | 
			
		||||
    @Override
 | 
			
		||||
    public void load(CompoundTag nbt) {
 | 
			
		||||
        super.load(nbt);
 | 
			
		||||
        // Fallback to the previous (incorrect) key
 | 
			
		||||
        peripheralAccessAllowed = nbt.getBoolean(NBT_PERIPHERAL_ENABLED) || nbt.getBoolean("PeirpheralAccess");
 | 
			
		||||
        peripheral.read(nbt, "");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void saveAdditional(CompoundTag nbt) {
 | 
			
		||||
        nbt.putBoolean(NBT_PERIPHERAL_ENABLED, peripheralAccessAllowed);
 | 
			
		||||
        peripheral.write(nbt, "");
 | 
			
		||||
        super.saveAdditional(nbt);
 | 
			
		||||
    }
 | 
			
		||||
@@ -213,7 +160,7 @@ public class CableBlockEntity extends BlockEntity {
 | 
			
		||||
        var state = getBlockState();
 | 
			
		||||
        var oldVariant = state.getValue(CableBlock.MODEM);
 | 
			
		||||
        var newVariant = CableModemVariant
 | 
			
		||||
            .from(oldVariant.getFacing(), modem.getModemState().isOpen(), peripheralAccessAllowed);
 | 
			
		||||
            .from(oldVariant.getFacing(), modem.getModemState().isOpen(), peripheral.hasPeripheral());
 | 
			
		||||
 | 
			
		||||
        if (oldVariant != newVariant) {
 | 
			
		||||
            level.setBlockAndUpdate(getBlockPos(), state.setValue(CableBlock.MODEM, newVariant));
 | 
			
		||||
@@ -223,31 +170,24 @@ public class CableBlockEntity extends BlockEntity {
 | 
			
		||||
    void blockTick() {
 | 
			
		||||
        if (getLevel().isClientSide) return;
 | 
			
		||||
 | 
			
		||||
        if (invalidPeripheral) refreshPeripheral();
 | 
			
		||||
        if (refreshPeripheral) {
 | 
			
		||||
            refreshPeripheral = false;
 | 
			
		||||
            if (isPeripheralOn()) attachPeripheral();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (modem.getModemState().pollChanged()) updateBlockState();
 | 
			
		||||
 | 
			
		||||
        if (!connectionsFormed) {
 | 
			
		||||
            connectionsFormed = true;
 | 
			
		||||
 | 
			
		||||
            connectionsChanged();
 | 
			
		||||
            if (peripheralAccessAllowed) {
 | 
			
		||||
                peripheral.attach(level, worldPosition, getDirection());
 | 
			
		||||
                updateConnectedPeripherals();
 | 
			
		||||
            }
 | 
			
		||||
        if (refreshConnections) connectionsChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        if (connectionsChanged) connectionsChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void scheduleConnectionsChanged() {
 | 
			
		||||
        connectionsChanged = true;
 | 
			
		||||
    void scheduleConnectionsChanged() {
 | 
			
		||||
        refreshConnections = true;
 | 
			
		||||
        TickScheduler.schedule(tickToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void connectionsChanged() {
 | 
			
		||||
        if (getLevel().isClientSide) return;
 | 
			
		||||
        connectionsChanged = false;
 | 
			
		||||
        refreshConnections = false;
 | 
			
		||||
 | 
			
		||||
        var state = getBlockState();
 | 
			
		||||
        var world = getLevel();
 | 
			
		||||
@@ -263,56 +203,29 @@ public class CableBlockEntity extends BlockEntity {
 | 
			
		||||
            if (CableBlock.canConnectIn(state, facing)) {
 | 
			
		||||
                // If we can connect to it then do so
 | 
			
		||||
                this.node.connectTo(node);
 | 
			
		||||
            } else if (this.node.getNetwork() == node.getNetwork()) {
 | 
			
		||||
                // Otherwise if we're on the same network then attempt to void it.
 | 
			
		||||
            } else {
 | 
			
		||||
                // Otherwise break the connection.
 | 
			
		||||
                this.node.disconnectFrom(node);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If we can no longer attach peripherals, then detach any which may have existed
 | 
			
		||||
        if (!canAttachPeripheral()) detachPeripheral();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void modemChanged() {
 | 
			
		||||
        // Tell anyone who cares that the connection state has changed
 | 
			
		||||
        PlatformHelper.get().invalidateComponent(this);
 | 
			
		||||
 | 
			
		||||
        if (getLevel().isClientSide) return;
 | 
			
		||||
 | 
			
		||||
        // If we can no longer attach peripherals, then detach any
 | 
			
		||||
        // which may have existed
 | 
			
		||||
        if (!canAttachPeripheral() && peripheralAccessAllowed) {
 | 
			
		||||
            peripheralAccessAllowed = false;
 | 
			
		||||
            peripheral.detach();
 | 
			
		||||
            node.updatePeripherals(Map.of());
 | 
			
		||||
            setChanged();
 | 
			
		||||
    private void attachPeripheral() {
 | 
			
		||||
        var dir = Objects.requireNonNull(getModemDirection(), "Attaching without a modem");
 | 
			
		||||
        if (peripheral.attach(getLevel(), getBlockPos(), dir)) updateConnectedPeripherals();
 | 
			
		||||
        updateBlockState();
 | 
			
		||||
    }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void togglePeripheralAccess() {
 | 
			
		||||
        if (!peripheralAccessAllowed) {
 | 
			
		||||
            peripheral.attach(level, getBlockPos(), getDirection());
 | 
			
		||||
            if (!peripheral.hasPeripheral()) return;
 | 
			
		||||
 | 
			
		||||
            peripheralAccessAllowed = true;
 | 
			
		||||
            node.updatePeripherals(peripheral.toMap());
 | 
			
		||||
        } else {
 | 
			
		||||
            peripheral.detach();
 | 
			
		||||
 | 
			
		||||
            peripheralAccessAllowed = false;
 | 
			
		||||
            node.updatePeripherals(Map.of());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    private void detachPeripheral() {
 | 
			
		||||
        if (peripheral.detach()) updateConnectedPeripherals();
 | 
			
		||||
        updateBlockState();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void updateConnectedPeripherals() {
 | 
			
		||||
        var peripherals = peripheral.toMap();
 | 
			
		||||
        if (peripherals.isEmpty()) {
 | 
			
		||||
            // If there are no peripherals then disable access and update the display state.
 | 
			
		||||
            peripheralAccessAllowed = false;
 | 
			
		||||
            updateBlockState();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        node.updatePeripherals(peripherals);
 | 
			
		||||
        node.updatePeripherals(peripheral.toMap());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
@@ -322,7 +235,11 @@ public class CableBlockEntity extends BlockEntity {
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public IPeripheral getPeripheral(@Nullable Direction direction) {
 | 
			
		||||
        return direction == null || getMaybeDirection() == direction ? modem : null;
 | 
			
		||||
        return direction == null || getModemDirection() == direction ? modem : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private boolean isPeripheralOn() {
 | 
			
		||||
        return getBlockState().getValue(CableBlock.MODEM).isPeripheralOn();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    boolean hasCable() {
 | 
			
		||||
 
 | 
			
		||||
@@ -31,15 +31,12 @@ public abstract class CableBlockItem extends BlockItem {
 | 
			
		||||
        // TODO: Check entity collision.
 | 
			
		||||
        if (!state.canSurvive(world, pos)) return false;
 | 
			
		||||
 | 
			
		||||
        world.setBlock(pos, state, 3);
 | 
			
		||||
        world.setBlockAndUpdate(pos, state);
 | 
			
		||||
        var soundType = state.getBlock().getSoundType(state);
 | 
			
		||||
        world.playSound(null, pos, soundType.getPlaceSound(), SoundSource.BLOCKS, (soundType.getVolume() + 1.0F) / 2.0F, soundType.getPitch() * 0.8F);
 | 
			
		||||
 | 
			
		||||
        var tile = world.getBlockEntity(pos);
 | 
			
		||||
        if (tile instanceof CableBlockEntity cable) {
 | 
			
		||||
            cable.modemChanged();
 | 
			
		||||
            cable.connectionsChanged();
 | 
			
		||||
        }
 | 
			
		||||
        if (tile instanceof CableBlockEntity cable) cable.connectionsChanged();
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -10,49 +10,57 @@ import net.minecraft.util.StringRepresentable;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
public enum CableModemVariant implements StringRepresentable {
 | 
			
		||||
    None("none", null),
 | 
			
		||||
    DownOff("down_off", Direction.DOWN),
 | 
			
		||||
    UpOff("up_off", Direction.UP),
 | 
			
		||||
    NorthOff("north_off", Direction.NORTH),
 | 
			
		||||
    SouthOff("south_off", Direction.SOUTH),
 | 
			
		||||
    WestOff("west_off", Direction.WEST),
 | 
			
		||||
    EastOff("east_off", Direction.EAST),
 | 
			
		||||
    DownOn("down_on", Direction.DOWN),
 | 
			
		||||
    UpOn("up_on", Direction.UP),
 | 
			
		||||
    NorthOn("north_on", Direction.NORTH),
 | 
			
		||||
    SouthOn("south_on", Direction.SOUTH),
 | 
			
		||||
    WestOn("west_on", Direction.WEST),
 | 
			
		||||
    EastOn("east_on", Direction.EAST),
 | 
			
		||||
    DownOffPeripheral("down_off_peripheral", Direction.DOWN),
 | 
			
		||||
    UpOffPeripheral("up_off_peripheral", Direction.UP),
 | 
			
		||||
    NorthOffPeripheral("north_off_peripheral", Direction.NORTH),
 | 
			
		||||
    SouthOffPeripheral("south_off_peripheral", Direction.SOUTH),
 | 
			
		||||
    WestOffPeripheral("west_off_peripheral", Direction.WEST),
 | 
			
		||||
    EastOffPeripheral("east_off_peripheral", Direction.EAST),
 | 
			
		||||
    DownOnPeripheral("down_on_peripheral", Direction.DOWN),
 | 
			
		||||
    UpOnPeripheral("up_on_peripheral", Direction.UP),
 | 
			
		||||
    NorthOnPeripheral("north_on_peripheral", Direction.NORTH),
 | 
			
		||||
    SouthOnPeripheral("south_on_peripheral", Direction.SOUTH),
 | 
			
		||||
    WestOnPeripheral("west_on_peripheral", Direction.WEST),
 | 
			
		||||
    EastOnPeripheral("east_on_peripheral", Direction.EAST);
 | 
			
		||||
    None("none", null, false, false),
 | 
			
		||||
    DownOff("down_off", Direction.DOWN, false, false),
 | 
			
		||||
    UpOff("up_off", Direction.UP, false, false),
 | 
			
		||||
    NorthOff("north_off", Direction.NORTH, false, false),
 | 
			
		||||
    SouthOff("south_off", Direction.SOUTH, false, false),
 | 
			
		||||
    WestOff("west_off", Direction.WEST, false, false),
 | 
			
		||||
    EastOff("east_off", Direction.EAST, false, false),
 | 
			
		||||
    DownOn("down_on", Direction.DOWN, true, false),
 | 
			
		||||
    UpOn("up_on", Direction.UP, true, false),
 | 
			
		||||
    NorthOn("north_on", Direction.NORTH, true, false),
 | 
			
		||||
    SouthOn("south_on", Direction.SOUTH, true, false),
 | 
			
		||||
    WestOn("west_on", Direction.WEST, true, false),
 | 
			
		||||
    EastOn("east_on", Direction.EAST, true, false),
 | 
			
		||||
    DownOffPeripheral("down_off_peripheral", Direction.DOWN, false, true),
 | 
			
		||||
    UpOffPeripheral("up_off_peripheral", Direction.UP, false, true),
 | 
			
		||||
    NorthOffPeripheral("north_off_peripheral", Direction.NORTH, false, true),
 | 
			
		||||
    SouthOffPeripheral("south_off_peripheral", Direction.SOUTH, false, true),
 | 
			
		||||
    WestOffPeripheral("west_off_peripheral", Direction.WEST, false, true),
 | 
			
		||||
    EastOffPeripheral("east_off_peripheral", Direction.EAST, false, true),
 | 
			
		||||
    DownOnPeripheral("down_on_peripheral", Direction.DOWN, true, true),
 | 
			
		||||
    UpOnPeripheral("up_on_peripheral", Direction.UP, true, true),
 | 
			
		||||
    NorthOnPeripheral("north_on_peripheral", Direction.NORTH, true, true),
 | 
			
		||||
    SouthOnPeripheral("south_on_peripheral", Direction.SOUTH, true, true),
 | 
			
		||||
    WestOnPeripheral("west_on_peripheral", Direction.WEST, true, true),
 | 
			
		||||
    EastOnPeripheral("east_on_peripheral", Direction.EAST, true, true);
 | 
			
		||||
 | 
			
		||||
    private static final CableModemVariant[] VALUES = values();
 | 
			
		||||
 | 
			
		||||
    private final String name;
 | 
			
		||||
    private final @Nullable Direction facing;
 | 
			
		||||
    private final boolean modemOn, peripheralOn;
 | 
			
		||||
 | 
			
		||||
    CableModemVariant(String name, @Nullable Direction facing) {
 | 
			
		||||
    CableModemVariant(String name, @Nullable Direction facing, boolean modemOn, boolean peripheralOn) {
 | 
			
		||||
        this.name = name;
 | 
			
		||||
        this.facing = facing;
 | 
			
		||||
        this.modemOn = modemOn;
 | 
			
		||||
        this.peripheralOn = peripheralOn;
 | 
			
		||||
        if (ordinal() != getIndex(facing, modemOn, peripheralOn)) throw new IllegalStateException("Mismatched ordinal");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static CableModemVariant from(Direction facing) {
 | 
			
		||||
        return facing == null ? None : VALUES[1 + facing.get3DDataValue()];
 | 
			
		||||
        return VALUES[1 + facing.get3DDataValue()];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static int getIndex(@Nullable Direction facing, boolean modem, boolean peripheral) {
 | 
			
		||||
        var state = (modem ? 1 : 0) + (peripheral ? 2 : 0);
 | 
			
		||||
        return facing == null ? 0 : 1 + 6 * state + facing.get3DDataValue();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static CableModemVariant from(@Nullable Direction facing, boolean modem, boolean peripheral) {
 | 
			
		||||
        var state = (modem ? 1 : 0) + (peripheral ? 2 : 0);
 | 
			
		||||
        return facing == null ? None : VALUES[1 + 6 * state + facing.get3DDataValue()];
 | 
			
		||||
        return VALUES[getIndex(facing, modem, peripheral)];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -64,6 +72,14 @@ public enum CableModemVariant implements StringRepresentable {
 | 
			
		||||
        return facing;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean isModemOn() {
 | 
			
		||||
        return modemOn;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean isPeripheralOn() {
 | 
			
		||||
        return peripheralOn;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String toString() {
 | 
			
		||||
        return name;
 | 
			
		||||
 
 | 
			
		||||
@@ -7,12 +7,14 @@ package dan200.computercraft.shared.peripheral.modem.wired;
 | 
			
		||||
import dan200.computercraft.annotations.ForgeOverride;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.core.Direction;
 | 
			
		||||
import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
import net.minecraft.util.RandomSource;
 | 
			
		||||
import net.minecraft.world.InteractionHand;
 | 
			
		||||
import net.minecraft.world.InteractionResult;
 | 
			
		||||
import net.minecraft.world.entity.player.Player;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
import net.minecraft.world.level.LevelAccessor;
 | 
			
		||||
import net.minecraft.world.level.LevelReader;
 | 
			
		||||
import net.minecraft.world.level.block.Block;
 | 
			
		||||
import net.minecraft.world.level.block.EntityBlock;
 | 
			
		||||
@@ -49,13 +51,27 @@ public class WiredModemFullBlock extends Block implements EntityBlock {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    @Deprecated
 | 
			
		||||
    public final void neighborChanged(BlockState state, Level world, BlockPos pos, Block neighbourBlock, BlockPos neighbourPos, boolean isMoving) {
 | 
			
		||||
        if (world.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) modem.neighborChanged(neighbourPos);
 | 
			
		||||
    public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor level, BlockPos pos, BlockPos neighborPos) {
 | 
			
		||||
        if (state.getValue(PERIPHERAL_ON) && level.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) {
 | 
			
		||||
            modem.queueRefreshPeripheral(direction);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return super.updateShape(state, direction, neighborState, level, pos, neighborPos);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    @Deprecated
 | 
			
		||||
    public final void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighbourBlock, BlockPos neighbourPos, boolean isMoving) {
 | 
			
		||||
        if (state.getValue(PERIPHERAL_ON) && level.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) {
 | 
			
		||||
            modem.neighborChanged(neighbourPos);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @ForgeOverride
 | 
			
		||||
    public final void onNeighborChange(BlockState state, LevelReader world, BlockPos pos, BlockPos neighbour) {
 | 
			
		||||
        if (world.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) modem.neighborChanged(neighbour);
 | 
			
		||||
    public final void onNeighborChange(BlockState state, LevelReader level, BlockPos pos, BlockPos neighbour) {
 | 
			
		||||
        if (state.getValue(PERIPHERAL_ON) && level.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) {
 | 
			
		||||
            modem.neighborChanged(neighbour);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
 
 | 
			
		||||
@@ -32,8 +32,6 @@ import static dan200.computercraft.shared.peripheral.modem.wired.WiredModemFullB
 | 
			
		||||
import static dan200.computercraft.shared.peripheral.modem.wired.WiredModemFullBlock.PERIPHERAL_ON;
 | 
			
		||||
 | 
			
		||||
public class WiredModemFullBlockEntity extends BlockEntity {
 | 
			
		||||
    private static final String NBT_PERIPHERAL_ENABLED = "PeripheralAccess";
 | 
			
		||||
 | 
			
		||||
    private static final class FullElement extends WiredModemElement {
 | 
			
		||||
        private final WiredModemFullBlockEntity entity;
 | 
			
		||||
 | 
			
		||||
@@ -70,11 +68,9 @@ public class WiredModemFullBlockEntity extends BlockEntity {
 | 
			
		||||
 | 
			
		||||
    private final WiredModemPeripheral[] modems = new WiredModemPeripheral[6];
 | 
			
		||||
 | 
			
		||||
    private boolean peripheralAccessAllowed = false;
 | 
			
		||||
    private final WiredModemLocalPeripheral[] peripherals = new WiredModemLocalPeripheral[6];
 | 
			
		||||
 | 
			
		||||
    private boolean connectionsFormed = false;
 | 
			
		||||
    private boolean connectionsChanged = false;
 | 
			
		||||
    private boolean refreshConnections = false;
 | 
			
		||||
 | 
			
		||||
    private final TickScheduler.Token tickToken = new TickScheduler.Token(this);
 | 
			
		||||
    private final ModemState modemState = new ModemState(() -> TickScheduler.schedule(tickToken));
 | 
			
		||||
@@ -96,31 +92,30 @@ public class WiredModemFullBlockEntity extends BlockEntity {
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setRemoved() {
 | 
			
		||||
        super.setRemoved();
 | 
			
		||||
        if (level == null || !level.isClientSide) {
 | 
			
		||||
            node.remove();
 | 
			
		||||
            connectionsFormed = false;
 | 
			
		||||
 | 
			
		||||
        for (var modem : modems) {
 | 
			
		||||
            if (modem != null) modem.removed();
 | 
			
		||||
        }
 | 
			
		||||
        if (level == null || !level.isClientSide) node.remove();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void clearRemoved() {
 | 
			
		||||
        super.clearRemoved();
 | 
			
		||||
        refreshConnections = true;
 | 
			
		||||
        invalidSides = DirectionUtil.ALL_SIDES;
 | 
			
		||||
        TickScheduler.schedule(tickToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void neighborChanged(BlockPos neighbour) {
 | 
			
		||||
        if (!level.isClientSide && peripheralAccessAllowed) {
 | 
			
		||||
        for (var facing : DirectionUtil.FACINGS) {
 | 
			
		||||
            if (getBlockPos().relative(facing).equals(neighbour)) queueRefreshPeripheral(facing);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void queueRefreshPeripheral(Direction facing) {
 | 
			
		||||
        if (invalidSides == 0) TickScheduler.schedule(tickToken);
 | 
			
		||||
    void queueRefreshPeripheral(Direction facing) {
 | 
			
		||||
        invalidSides |= 1 << facing.ordinal();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void refreshPeripheral(Direction facing) {
 | 
			
		||||
        invalidSides &= ~(1 << facing.ordinal());
 | 
			
		||||
        var peripheral = peripherals[facing.ordinal()];
 | 
			
		||||
        if (level != null && !isRemoved() && peripheral.attach(level, getBlockPos(), facing)) {
 | 
			
		||||
            updateConnectedPeripherals();
 | 
			
		||||
        }
 | 
			
		||||
        TickScheduler.schedule(tickToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public InteractionResult use(Player player) {
 | 
			
		||||
@@ -129,7 +124,11 @@ public class WiredModemFullBlockEntity extends BlockEntity {
 | 
			
		||||
 | 
			
		||||
        // On server, we interacted if a peripheral was found
 | 
			
		||||
        var oldPeriphNames = getConnectedPeripheralNames();
 | 
			
		||||
        togglePeripheralAccess();
 | 
			
		||||
        if (isPeripheralOn()) {
 | 
			
		||||
            detachPeripherals();
 | 
			
		||||
        } else {
 | 
			
		||||
            attachPeripherals(DirectionUtil.ALL_SIDES);
 | 
			
		||||
        }
 | 
			
		||||
        var periphNames = getConnectedPeripheralNames();
 | 
			
		||||
 | 
			
		||||
        if (!Objects.equals(periphNames, oldPeriphNames)) {
 | 
			
		||||
@@ -158,65 +157,45 @@ public class WiredModemFullBlockEntity extends BlockEntity {
 | 
			
		||||
    @Override
 | 
			
		||||
    public void load(CompoundTag nbt) {
 | 
			
		||||
        super.load(nbt);
 | 
			
		||||
        peripheralAccessAllowed = nbt.getBoolean(NBT_PERIPHERAL_ENABLED);
 | 
			
		||||
        for (var i = 0; i < peripherals.length; i++) peripherals[i].read(nbt, Integer.toString(i));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void saveAdditional(CompoundTag nbt) {
 | 
			
		||||
        nbt.putBoolean(NBT_PERIPHERAL_ENABLED, peripheralAccessAllowed);
 | 
			
		||||
        for (var i = 0; i < peripherals.length; i++) peripherals[i].write(nbt, Integer.toString(i));
 | 
			
		||||
        super.saveAdditional(nbt);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void updateBlockState() {
 | 
			
		||||
        var state = getBlockState();
 | 
			
		||||
        boolean modemOn = modemState.isOpen(), peripheralOn = peripheralAccessAllowed;
 | 
			
		||||
        if (state.getValue(MODEM_ON) == modemOn && state.getValue(PERIPHERAL_ON) == peripheralOn) return;
 | 
			
		||||
 | 
			
		||||
        getLevel().setBlockAndUpdate(getBlockPos(), state.setValue(MODEM_ON, modemOn).setValue(PERIPHERAL_ON, peripheralOn));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void clearRemoved() {
 | 
			
		||||
        super.clearRemoved();
 | 
			
		||||
        TickScheduler.schedule(tickToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void blockTick() {
 | 
			
		||||
        if (getLevel().isClientSide) return;
 | 
			
		||||
 | 
			
		||||
        if (invalidSides != 0) {
 | 
			
		||||
            for (var direction : DirectionUtil.FACINGS) {
 | 
			
		||||
                if ((invalidSides & (1 << direction.ordinal())) != 0) refreshPeripheral(direction);
 | 
			
		||||
            }
 | 
			
		||||
            var oldInvalidSides = invalidSides;
 | 
			
		||||
            invalidSides = 0;
 | 
			
		||||
            if (isPeripheralOn()) attachPeripherals(oldInvalidSides);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (modemState.pollChanged()) updateBlockState();
 | 
			
		||||
        if (modemState.pollChanged()) updateModemBlockState();
 | 
			
		||||
 | 
			
		||||
        if (!connectionsFormed) {
 | 
			
		||||
            connectionsFormed = true;
 | 
			
		||||
 | 
			
		||||
            connectionsChanged();
 | 
			
		||||
            if (peripheralAccessAllowed) {
 | 
			
		||||
                for (var facing : DirectionUtil.FACINGS) {
 | 
			
		||||
                    peripherals[facing.ordinal()].attach(level, getBlockPos(), facing);
 | 
			
		||||
                }
 | 
			
		||||
                updateConnectedPeripherals();
 | 
			
		||||
            }
 | 
			
		||||
        if (refreshConnections) connectionsChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        if (connectionsChanged) connectionsChanged();
 | 
			
		||||
    private void updateModemBlockState() {
 | 
			
		||||
        var state = getBlockState();
 | 
			
		||||
        var modemOn = modemState.isOpen();
 | 
			
		||||
        if (state.getValue(MODEM_ON) == modemOn) return;
 | 
			
		||||
 | 
			
		||||
        getLevel().setBlockAndUpdate(getBlockPos(), state.setValue(MODEM_ON, modemOn));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void scheduleConnectionsChanged() {
 | 
			
		||||
        connectionsChanged = true;
 | 
			
		||||
        refreshConnections = true;
 | 
			
		||||
        TickScheduler.schedule(tickToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void connectionsChanged() {
 | 
			
		||||
        if (getLevel().isClientSide) return;
 | 
			
		||||
        connectionsChanged = false;
 | 
			
		||||
        refreshConnections = false;
 | 
			
		||||
 | 
			
		||||
        var world = getLevel();
 | 
			
		||||
        var current = getBlockPos();
 | 
			
		||||
@@ -231,57 +210,48 @@ public class WiredModemFullBlockEntity extends BlockEntity {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void togglePeripheralAccess() {
 | 
			
		||||
        if (!peripheralAccessAllowed) {
 | 
			
		||||
            var hasAny = false;
 | 
			
		||||
            for (var facing : DirectionUtil.FACINGS) {
 | 
			
		||||
                var peripheral = peripherals[facing.ordinal()];
 | 
			
		||||
                peripheral.attach(level, getBlockPos(), facing);
 | 
			
		||||
                hasAny |= peripheral.hasPeripheral();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!hasAny) return;
 | 
			
		||||
 | 
			
		||||
            peripheralAccessAllowed = true;
 | 
			
		||||
            node.updatePeripherals(getConnectedPeripherals());
 | 
			
		||||
        } else {
 | 
			
		||||
            peripheralAccessAllowed = false;
 | 
			
		||||
 | 
			
		||||
            for (var peripheral : peripherals) peripheral.detach();
 | 
			
		||||
            node.updatePeripherals(Map.of());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        updateBlockState();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Set<String> getConnectedPeripheralNames() {
 | 
			
		||||
        if (!peripheralAccessAllowed) return Set.of();
 | 
			
		||||
 | 
			
		||||
        Set<String> peripherals = new HashSet<>(6);
 | 
			
		||||
    private List<String> getConnectedPeripheralNames() {
 | 
			
		||||
        List<String> peripherals = new ArrayList<>(6);
 | 
			
		||||
        for (var peripheral : this.peripherals) {
 | 
			
		||||
            var name = peripheral.getConnectedName();
 | 
			
		||||
            if (name != null) peripherals.add(name);
 | 
			
		||||
        }
 | 
			
		||||
        peripherals.sort(String::compareTo);
 | 
			
		||||
        return peripherals;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Map<String, IPeripheral> getConnectedPeripherals() {
 | 
			
		||||
        if (!peripheralAccessAllowed) return Map.of();
 | 
			
		||||
    private void attachPeripherals(int sides) {
 | 
			
		||||
        var anyChanged = false;
 | 
			
		||||
 | 
			
		||||
        Map<String, IPeripheral> peripherals = new HashMap<>(6);
 | 
			
		||||
        for (var peripheral : this.peripherals) peripheral.extendMap(peripherals);
 | 
			
		||||
        return Collections.unmodifiableMap(peripherals);
 | 
			
		||||
        Map<String, IPeripheral> attachedPeripherals = new HashMap<>(6);
 | 
			
		||||
 | 
			
		||||
        for (var facing : DirectionUtil.FACINGS) {
 | 
			
		||||
            var peripheral = peripherals[facing.ordinal()];
 | 
			
		||||
            if (DirectionUtil.isSet(sides, facing)) anyChanged |= peripheral.attach(getLevel(), getBlockPos(), facing);
 | 
			
		||||
            peripheral.extendMap(attachedPeripherals);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    private void updateConnectedPeripherals() {
 | 
			
		||||
        var peripherals = getConnectedPeripherals();
 | 
			
		||||
        if (peripherals.isEmpty()) {
 | 
			
		||||
            // If there are no peripherals then disable access and update the display state.
 | 
			
		||||
            peripheralAccessAllowed = false;
 | 
			
		||||
            updateBlockState();
 | 
			
		||||
        if (anyChanged) node.updatePeripherals(attachedPeripherals);
 | 
			
		||||
 | 
			
		||||
        updatePeripheralBlocKState(!attachedPeripherals.isEmpty());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        node.updatePeripherals(peripherals);
 | 
			
		||||
    private void detachPeripherals() {
 | 
			
		||||
        var anyChanged = false;
 | 
			
		||||
        for (var peripheral : peripherals) anyChanged |= peripheral.detach();
 | 
			
		||||
        if (anyChanged) node.updatePeripherals(Map.of());
 | 
			
		||||
 | 
			
		||||
        updatePeripheralBlocKState(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void updatePeripheralBlocKState(boolean peripheralOn) {
 | 
			
		||||
        var state = getBlockState();
 | 
			
		||||
        if (state.getValue(PERIPHERAL_ON) == peripheralOn) return;
 | 
			
		||||
        getLevel().setBlockAndUpdate(getBlockPos(), state.setValue(PERIPHERAL_ON, peripheralOn));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private boolean isPeripheralOn() {
 | 
			
		||||
        return getBlockState().getValue(PERIPHERAL_ON);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public WiredElement getElement() {
 | 
			
		||||
@@ -295,22 +265,11 @@ public class WiredModemFullBlockEntity extends BlockEntity {
 | 
			
		||||
        var peripheral = modems[side.ordinal()];
 | 
			
		||||
        if (peripheral != null) return peripheral;
 | 
			
		||||
 | 
			
		||||
        var localPeripheral = peripherals[side.ordinal()];
 | 
			
		||||
        return modems[side.ordinal()] = new WiredModemPeripheral(modemState, element) {
 | 
			
		||||
            @Override
 | 
			
		||||
            protected WiredModemLocalPeripheral getLocalPeripheral() {
 | 
			
		||||
                return localPeripheral;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        return modems[side.ordinal()] = new WiredModemPeripheral(modemState, element, peripherals[side.ordinal()], this) {
 | 
			
		||||
            @Override
 | 
			
		||||
            public Vec3 getPosition() {
 | 
			
		||||
                return Vec3.atCenterOf(getBlockPos().relative(side));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public Object getTarget() {
 | 
			
		||||
                return WiredModemFullBlockEntity.this;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ package dan200.computercraft.shared.peripheral.modem.wired;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftTags;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import dan200.computercraft.core.util.PeripheralHelpers;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ServerContext;
 | 
			
		||||
import dan200.computercraft.shared.platform.ComponentAccess;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
@@ -15,7 +16,6 @@ import net.minecraft.nbt.Tag;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.core.util.Nullability.assertNonNull;
 | 
			
		||||
@@ -66,7 +66,7 @@ public final class WiredModemLocalPeripheral {
 | 
			
		||||
                this.id = ServerContext.get(assertNonNull(world.getServer())).getNextId("peripheral." + type);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return oldPeripheral == null || !oldPeripheral.equals(peripheral);
 | 
			
		||||
            return !PeripheralHelpers.equals(oldPeripheral, peripheral);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -86,11 +86,6 @@ public final class WiredModemLocalPeripheral {
 | 
			
		||||
        return peripheral != null ? type + "_" + id : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public IPeripheral getPeripheral() {
 | 
			
		||||
        return peripheral;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean hasPeripheral() {
 | 
			
		||||
        return peripheral != null;
 | 
			
		||||
    }
 | 
			
		||||
@@ -100,9 +95,7 @@ public final class WiredModemLocalPeripheral {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Map<String, IPeripheral> toMap() {
 | 
			
		||||
        return peripheral == null
 | 
			
		||||
            ? Map.of()
 | 
			
		||||
            : Collections.singletonMap(type + "_" + id, peripheral);
 | 
			
		||||
        return peripheral == null ? Map.of() : Map.of(type + "_" + id, peripheral);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void write(CompoundTag tag, String suffix) {
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import dan200.computercraft.api.peripheral.NotAttachedException;
 | 
			
		||||
import dan200.computercraft.api.peripheral.WorkMonitor;
 | 
			
		||||
import dan200.computercraft.core.apis.PeripheralAPI;
 | 
			
		||||
import dan200.computercraft.core.computer.GuardedLuaContext;
 | 
			
		||||
import dan200.computercraft.core.methods.PeripheralMethod;
 | 
			
		||||
import dan200.computercraft.core.util.LuaUtil;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ServerContext;
 | 
			
		||||
@@ -22,6 +23,7 @@ import dan200.computercraft.shared.peripheral.modem.ModemPeripheral;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.modem.ModemState;
 | 
			
		||||
import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntity;
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
 | 
			
		||||
@@ -34,12 +36,21 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
 | 
			
		||||
    private static final Logger LOG = LoggerFactory.getLogger(WiredModemPeripheral.class);
 | 
			
		||||
 | 
			
		||||
    private final WiredModemElement modem;
 | 
			
		||||
    private final WiredModemLocalPeripheral localPeripheral;
 | 
			
		||||
    private final BlockEntity target;
 | 
			
		||||
 | 
			
		||||
    private final Map<IComputerAccess, ConcurrentMap<String, RemotePeripheralWrapper>> peripheralWrappers = new HashMap<>(1);
 | 
			
		||||
 | 
			
		||||
    public WiredModemPeripheral(ModemState state, WiredModemElement modem) {
 | 
			
		||||
    public WiredModemPeripheral(
 | 
			
		||||
        ModemState state,
 | 
			
		||||
        WiredModemElement modem,
 | 
			
		||||
        WiredModemLocalPeripheral localPeripheral,
 | 
			
		||||
        BlockEntity target
 | 
			
		||||
    ) {
 | 
			
		||||
        super(state);
 | 
			
		||||
        this.modem = modem;
 | 
			
		||||
        this.localPeripheral = localPeripheral;
 | 
			
		||||
        this.target = target;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //region IPacketSender implementation
 | 
			
		||||
@@ -62,8 +73,6 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
 | 
			
		||||
    public Level getLevel() {
 | 
			
		||||
        return modem.getLevel();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected abstract WiredModemLocalPeripheral getLocalPeripheral();
 | 
			
		||||
    //endregion
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -207,7 +216,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
 | 
			
		||||
     */
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final @Nullable Object[] getNameLocal() {
 | 
			
		||||
        var local = getLocalPeripheral().getConnectedName();
 | 
			
		||||
        var local = localPeripheral.getConnectedName();
 | 
			
		||||
        return local == null ? null : new Object[]{ local };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -218,8 +227,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
 | 
			
		||||
 | 
			
		||||
        ConcurrentMap<String, RemotePeripheralWrapper> wrappers;
 | 
			
		||||
        synchronized (peripheralWrappers) {
 | 
			
		||||
            wrappers = peripheralWrappers.get(computer);
 | 
			
		||||
            if (wrappers == null) peripheralWrappers.put(computer, wrappers = new ConcurrentHashMap<>());
 | 
			
		||||
            wrappers = peripheralWrappers.computeIfAbsent(computer, k -> new ConcurrentHashMap<>());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        synchronized (modem.getRemotePeripherals()) {
 | 
			
		||||
@@ -245,11 +253,13 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean equals(@Nullable IPeripheral other) {
 | 
			
		||||
        if (other instanceof WiredModemPeripheral otherModem) {
 | 
			
		||||
            return otherModem.modem == modem;
 | 
			
		||||
    public final boolean equals(@Nullable IPeripheral other) {
 | 
			
		||||
        return other instanceof WiredModemPeripheral otherModem && otherModem.modem == modem;
 | 
			
		||||
    }
 | 
			
		||||
        return false;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final Object getTarget() {
 | 
			
		||||
        return target;
 | 
			
		||||
    }
 | 
			
		||||
    //endregion
 | 
			
		||||
 | 
			
		||||
@@ -272,12 +282,11 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
 | 
			
		||||
                var wrapper = wrappers.remove(name);
 | 
			
		||||
                if (wrapper != null) wrapper.detach();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void attachPeripheralImpl(IComputerAccess computer, ConcurrentMap<String, RemotePeripheralWrapper> peripherals, String periphName, IPeripheral peripheral) {
 | 
			
		||||
        if (!peripherals.containsKey(periphName) && !periphName.equals(getLocalPeripheral().getConnectedName())) {
 | 
			
		||||
        if (!peripherals.containsKey(periphName) && !periphName.equals(localPeripheral.getConnectedName())) {
 | 
			
		||||
            var methods = ServerContext.get(((ServerLevel) getLevel()).getServer()).peripheralMethods().getSelfMethods(peripheral);
 | 
			
		||||
            var wrapper = new RemotePeripheralWrapper(modem, peripheral, computer, periphName, methods);
 | 
			
		||||
            peripherals.put(periphName, wrapper);
 | 
			
		||||
@@ -296,7 +305,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
 | 
			
		||||
        return wrappers == null ? null : wrappers.get(remoteName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static class RemotePeripheralWrapper implements IComputerAccess {
 | 
			
		||||
    private static class RemotePeripheralWrapper implements IComputerAccess, GuardedLuaContext.Guard {
 | 
			
		||||
        private final WiredModemElement element;
 | 
			
		||||
        private final IPeripheral peripheral;
 | 
			
		||||
        private final IComputerAccess computer;
 | 
			
		||||
@@ -309,6 +318,8 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
 | 
			
		||||
        private volatile boolean attached;
 | 
			
		||||
        private final Set<String> mounts = new HashSet<>();
 | 
			
		||||
 | 
			
		||||
        private @Nullable GuardedLuaContext contextWrapper;
 | 
			
		||||
 | 
			
		||||
        RemotePeripheralWrapper(WiredModemElement element, IPeripheral peripheral, IComputerAccess computer, String name, Map<String, PeripheralMethod> methods) {
 | 
			
		||||
            this.element = element;
 | 
			
		||||
            this.peripheral = peripheral;
 | 
			
		||||
@@ -356,7 +367,19 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
 | 
			
		||||
        public MethodResult callMethod(ILuaContext context, String methodName, IArguments arguments) throws LuaException {
 | 
			
		||||
            var method = methodMap.get(methodName);
 | 
			
		||||
            if (method == null) throw new LuaException("No such method " + methodName);
 | 
			
		||||
            return method.apply(peripheral, context, this, arguments);
 | 
			
		||||
 | 
			
		||||
            // Wrap the ILuaContext. We try to reuse the previous context where possible to avoid allocations.
 | 
			
		||||
            var contextWrapper = this.contextWrapper;
 | 
			
		||||
            if (contextWrapper == null || !contextWrapper.wraps(context)) {
 | 
			
		||||
                contextWrapper = this.contextWrapper = new GuardedLuaContext(context, this);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return method.apply(peripheral, contextWrapper, this, arguments);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public boolean checkValid() {
 | 
			
		||||
            return attached;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // IComputerAccess implementation
 | 
			
		||||
 
 | 
			
		||||
@@ -44,13 +44,18 @@ public class MonitorBlockEntity extends BlockEntity {
 | 
			
		||||
    private final boolean advanced;
 | 
			
		||||
 | 
			
		||||
    private @Nullable ServerMonitor serverMonitor;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The monitor's state on the client. This is defined iff we're the origin monitor
 | 
			
		||||
     * ({@code xIndex == 0 && yIndex == 0}).
 | 
			
		||||
     */
 | 
			
		||||
    private @Nullable ClientMonitor clientMonitor;
 | 
			
		||||
 | 
			
		||||
    private @Nullable MonitorPeripheral peripheral;
 | 
			
		||||
    private final Set<IComputerAccess> computers = Collections.newSetFromMap(new ConcurrentHashMap<>());
 | 
			
		||||
 | 
			
		||||
    private boolean needsUpdate = false;
 | 
			
		||||
    private boolean needsValidating = false;
 | 
			
		||||
    private boolean destroyed = false;
 | 
			
		||||
 | 
			
		||||
    // MonitorWatcher state.
 | 
			
		||||
    boolean enqueued;
 | 
			
		||||
@@ -89,7 +94,7 @@ public class MonitorBlockEntity extends BlockEntity {
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setRemoved() {
 | 
			
		||||
        super.setRemoved();
 | 
			
		||||
        if (clientMonitor != null && xIndex == 0 && yIndex == 0) clientMonitor.destroy();
 | 
			
		||||
        if (clientMonitor != null) clientMonitor.destroy();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -143,7 +148,7 @@ public class MonitorBlockEntity extends BlockEntity {
 | 
			
		||||
    private ServerMonitor getServerMonitor() {
 | 
			
		||||
        if (serverMonitor != null) return serverMonitor;
 | 
			
		||||
 | 
			
		||||
        var origin = getOrigin().getMonitor();
 | 
			
		||||
        var origin = getOrigin();
 | 
			
		||||
        if (origin == null) return null;
 | 
			
		||||
 | 
			
		||||
        return serverMonitor = origin.serverMonitor;
 | 
			
		||||
@@ -182,13 +187,11 @@ public class MonitorBlockEntity extends BlockEntity {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public ClientMonitor getClientMonitor() {
 | 
			
		||||
    public ClientMonitor getOriginClientMonitor() {
 | 
			
		||||
        if (clientMonitor != null) return clientMonitor;
 | 
			
		||||
 | 
			
		||||
        var te = level.getBlockEntity(toWorldPos(0, 0));
 | 
			
		||||
        if (!(te instanceof MonitorBlockEntity monitor)) return null;
 | 
			
		||||
 | 
			
		||||
        return clientMonitor = monitor.clientMonitor;
 | 
			
		||||
        var origin = getOrigin();
 | 
			
		||||
        return origin == null ? null : origin.clientMonitor;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Networking stuff
 | 
			
		||||
@@ -209,17 +212,14 @@ public class MonitorBlockEntity extends BlockEntity {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void onClientLoad(int oldXIndex, int oldYIndex) {
 | 
			
		||||
        if (oldXIndex != xIndex || oldYIndex != yIndex) {
 | 
			
		||||
            // If our index has changed then it's possible the origin monitor has changed. Thus
 | 
			
		||||
            // we'll clear our cache. If we're the origin then we'll need to remove the glList as well.
 | 
			
		||||
            if (oldXIndex == 0 && oldYIndex == 0 && clientMonitor != null) clientMonitor.destroy();
 | 
			
		||||
        if ((oldXIndex != xIndex || oldYIndex != yIndex) && clientMonitor != null) {
 | 
			
		||||
            // If our index has changed, and we were the origin, then destroy the current monitor.
 | 
			
		||||
            clientMonitor.destroy();
 | 
			
		||||
            clientMonitor = null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (xIndex == 0 && yIndex == 0) {
 | 
			
		||||
        // If we're the origin terminal then create it.
 | 
			
		||||
            if (clientMonitor == null) clientMonitor = new ClientMonitor(this);
 | 
			
		||||
        }
 | 
			
		||||
        if (xIndex == 0 && yIndex == 0 && clientMonitor == null) clientMonitor = new ClientMonitor(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public final void read(TerminalState state) {
 | 
			
		||||
@@ -286,7 +286,7 @@ public class MonitorBlockEntity extends BlockEntity {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    boolean isCompatible(MonitorBlockEntity other) {
 | 
			
		||||
        return !other.destroyed && advanced == other.advanced && getOrientation() == other.getOrientation() && getDirection() == other.getDirection();
 | 
			
		||||
        return advanced == other.advanced && getOrientation() == other.getOrientation() && getDirection() == other.getDirection();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -309,8 +309,8 @@ public class MonitorBlockEntity extends BlockEntity {
 | 
			
		||||
        return isCompatible(monitor) ? MonitorState.present(monitor) : MonitorState.MISSING;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private MonitorState getOrigin() {
 | 
			
		||||
        return getLoadedMonitor(0, 0);
 | 
			
		||||
    private @Nullable MonitorBlockEntity getOrigin() {
 | 
			
		||||
        return getLoadedMonitor(0, 0).getMonitor();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -389,7 +389,7 @@ public class MonitorBlockEntity extends BlockEntity {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void expand() {
 | 
			
		||||
        var monitor = getOrigin().getMonitor();
 | 
			
		||||
        var monitor = getOrigin();
 | 
			
		||||
        if (monitor != null && monitor.xIndex == 0 && monitor.yIndex == 0) new Expander(monitor).expand();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -558,7 +558,7 @@ public class MonitorBlockEntity extends BlockEntity {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var hasPeripheral = false;
 | 
			
		||||
        var origin = getOrigin().getMonitor();
 | 
			
		||||
        var origin = getOrigin();
 | 
			
		||||
        var serverMonitor = origin != null ? origin.serverMonitor : this.serverMonitor;
 | 
			
		||||
        for (var x = 0; x < width; x++) {
 | 
			
		||||
            for (var y = 0; y < height; y++) {
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,7 @@ class DfpwmState {
 | 
			
		||||
    private boolean unplayed = true;
 | 
			
		||||
    private long clientEndTime = PauseAwareTimer.getTime();
 | 
			
		||||
    private float pendingVolume = 1.0f;
 | 
			
		||||
    private @Nullable ByteBuffer pendingAudio;
 | 
			
		||||
    private @Nullable EncodedAudio pendingAudio;
 | 
			
		||||
 | 
			
		||||
    synchronized boolean pushBuffer(LuaTable<?, ?> table, int size, Optional<Double> volume) throws LuaException {
 | 
			
		||||
        if (pendingAudio != null) return false;
 | 
			
		||||
@@ -45,6 +45,10 @@ class DfpwmState {
 | 
			
		||||
        var outSize = size / 8;
 | 
			
		||||
        var buffer = ByteBuffer.allocate(outSize);
 | 
			
		||||
 | 
			
		||||
        var initialCharge = charge;
 | 
			
		||||
        var initialStrength = strength;
 | 
			
		||||
        var initialPreviousBit = previousBit;
 | 
			
		||||
 | 
			
		||||
        for (var i = 0; i < outSize; i++) {
 | 
			
		||||
            var thisByte = 0;
 | 
			
		||||
            for (var j = 1; j <= 8; j++) {
 | 
			
		||||
@@ -80,7 +84,7 @@ class DfpwmState {
 | 
			
		||||
 | 
			
		||||
        buffer.flip();
 | 
			
		||||
 | 
			
		||||
        pendingAudio = buffer;
 | 
			
		||||
        pendingAudio = new EncodedAudio(initialCharge, initialStrength, initialPreviousBit, buffer);
 | 
			
		||||
        pendingVolume = (float) clampVolume(volume.orElse((double) pendingVolume));
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
@@ -89,12 +93,12 @@ class DfpwmState {
 | 
			
		||||
        return pendingAudio != null && now >= clientEndTime - CLIENT_BUFFER;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ByteBuffer pullPending(long now) {
 | 
			
		||||
    EncodedAudio pullPending(long now) {
 | 
			
		||||
        var audio = pendingAudio;
 | 
			
		||||
        if (audio == null) throw new IllegalStateException("Should not pull pending audio yet");
 | 
			
		||||
        pendingAudio = null;
 | 
			
		||||
        // Compute when we should consider sending the next packet.
 | 
			
		||||
        clientEndTime = Math.max(now, clientEndTime) + (audio.remaining() * SECOND * 8 / SAMPLE_RATE);
 | 
			
		||||
        clientEndTime = Math.max(now, clientEndTime) + (audio.audio().remaining() * SECOND * 8 / SAMPLE_RATE);
 | 
			
		||||
        unplayed = false;
 | 
			
		||||
        return audio;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,37 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.peripheral.speaker;
 | 
			
		||||
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A chunk of encoded audio, along with the state required for the decoder to reproduce the original audio samples.
 | 
			
		||||
 *
 | 
			
		||||
 * @param charge      The DFPWM charge.
 | 
			
		||||
 * @param strength    The DFPWM strength.
 | 
			
		||||
 * @param previousBit The previous bit.
 | 
			
		||||
 * @param audio       The block of encoded audio.
 | 
			
		||||
 */
 | 
			
		||||
public record EncodedAudio(int charge, int strength, boolean previousBit, ByteBuffer audio) {
 | 
			
		||||
    public void write(FriendlyByteBuf buf) {
 | 
			
		||||
        buf.writeVarInt(charge());
 | 
			
		||||
        buf.writeVarInt(strength());
 | 
			
		||||
        buf.writeBoolean(previousBit());
 | 
			
		||||
        buf.writeBytes(audio().duplicate());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static EncodedAudio read(FriendlyByteBuf buf) {
 | 
			
		||||
        var charge = buf.readVarInt();
 | 
			
		||||
        var strength = buf.readVarInt();
 | 
			
		||||
        var previousBit = buf.readBoolean();
 | 
			
		||||
 | 
			
		||||
        var bytes = new byte[buf.readableBytes()];
 | 
			
		||||
        buf.readBytes(bytes);
 | 
			
		||||
 | 
			
		||||
        return new EncodedAudio(charge, strength, previousBit, ByteBuffer.wrap(bytes));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -375,4 +375,13 @@ public interface PlatformHelper extends dan200.computercraft.impl.PlatformHelper
 | 
			
		||||
     * @see ServerPlayerGameMode#useItemOn(ServerPlayer, Level, ItemStack, InteractionHand, BlockHitResult)
 | 
			
		||||
     */
 | 
			
		||||
    InteractionResult useOn(ServerPlayer player, ItemStack stack, BlockHitResult hit, Predicate<BlockState> canUseBlock);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether {@link net.minecraft.network.chat.ClickEvent.Action#RUN_COMMAND} can be used to run client commands.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Whether client commands can be triggered from chat components.
 | 
			
		||||
     */
 | 
			
		||||
    default boolean canClickRunClientCommand() {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import dan200.computercraft.api.upgrades.UpgradeData;
 | 
			
		||||
import dan200.computercraft.core.computer.ComputerSide;
 | 
			
		||||
import dan200.computercraft.shared.common.IColouredItem;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerState;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ServerComputer;
 | 
			
		||||
import dan200.computercraft.shared.config.Config;
 | 
			
		||||
import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
 | 
			
		||||
@@ -38,7 +39,10 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
 | 
			
		||||
    private ItemStack stack = ItemStack.EMPTY;
 | 
			
		||||
 | 
			
		||||
    private int lightColour = -1;
 | 
			
		||||
    private boolean lightChanged = false;
 | 
			
		||||
 | 
			
		||||
    // The state the previous tick, used to determine if the state needs to be sent to the client.
 | 
			
		||||
    private int oldLightColour = -1;
 | 
			
		||||
    private @Nullable ComputerState oldComputerState;
 | 
			
		||||
 | 
			
		||||
    private final Set<ServerPlayer> tracking = new HashSet<>();
 | 
			
		||||
 | 
			
		||||
@@ -83,10 +87,7 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setLight(int colour) {
 | 
			
		||||
        if (colour < 0 || colour > 0xFFFFFF) colour = -1;
 | 
			
		||||
 | 
			
		||||
        if (lightColour == colour) return;
 | 
			
		||||
        lightColour = colour;
 | 
			
		||||
        lightChanged = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -151,9 +152,11 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
 | 
			
		||||
        tracking.removeIf(player -> !player.isAlive() || player.level() != getLevel());
 | 
			
		||||
 | 
			
		||||
        // And now find any new players, add them to the tracking list, and broadcast state where appropriate.
 | 
			
		||||
        var sendState = hasOutputChanged() || lightChanged;
 | 
			
		||||
        lightChanged = false;
 | 
			
		||||
        if (sendState) {
 | 
			
		||||
        var state = getState();
 | 
			
		||||
        if (oldLightColour != lightColour || oldComputerState != state) {
 | 
			
		||||
            oldComputerState = state;
 | 
			
		||||
            oldLightColour = lightColour;
 | 
			
		||||
 | 
			
		||||
            // Broadcast the state to all players
 | 
			
		||||
            tracking.addAll(getLevel().players());
 | 
			
		||||
            ServerNetworking.sendToPlayers(new PocketComputerDataMessage(this, false), tracking);
 | 
			
		||||
@@ -182,6 +185,6 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onRemoved() {
 | 
			
		||||
        super.onRemoved();
 | 
			
		||||
        ServerNetworking.sendToAllPlayers(new PocketComputerDeletedClientMessage(getInstanceID()), getLevel().getServer());
 | 
			
		||||
        ServerNetworking.sendToAllPlayers(new PocketComputerDeletedClientMessage(getInstanceUUID()), getLevel().getServer());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -43,6 +43,7 @@ import net.minecraft.world.level.Level;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
 | 
			
		||||
public class PocketComputerItem extends Item implements IComputerItem, IMedia, IColouredItem {
 | 
			
		||||
    private static final String NBT_UPGRADE = "Upgrade";
 | 
			
		||||
@@ -188,10 +189,9 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public PocketServerComputer createServerComputer(ServerLevel level, Entity entity, @Nullable Container inventory, ItemStack stack) {
 | 
			
		||||
        var sessionID = getSessionID(stack);
 | 
			
		||||
 | 
			
		||||
        var registry = ServerContext.get(level.getServer()).registry();
 | 
			
		||||
        var computer = (PocketServerComputer) registry.get(sessionID, getInstanceID(stack));
 | 
			
		||||
        var computer = (PocketServerComputer) registry.get(getSessionID(stack), getInstanceID(stack));
 | 
			
		||||
        if (computer == null) {
 | 
			
		||||
            var computerID = getComputerID(stack);
 | 
			
		||||
            if (computerID < 0) {
 | 
			
		||||
@@ -201,8 +201,9 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
 | 
			
		||||
 | 
			
		||||
            computer = new PocketServerComputer(level, entity.blockPosition(), getComputerID(stack), getLabel(stack), getFamily());
 | 
			
		||||
 | 
			
		||||
            setInstanceID(stack, computer.register());
 | 
			
		||||
            setSessionID(stack, registry.getSessionID());
 | 
			
		||||
            var tag = stack.getOrCreateTag();
 | 
			
		||||
            tag.putInt(NBT_SESSION, registry.getSessionID());
 | 
			
		||||
            tag.putUUID(NBT_INSTANCE, computer.register());
 | 
			
		||||
 | 
			
		||||
            var upgrade = getUpgrade(stack);
 | 
			
		||||
 | 
			
		||||
@@ -267,13 +268,9 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static int getInstanceID(ItemStack stack) {
 | 
			
		||||
    public static @Nullable UUID getInstanceID(ItemStack stack) {
 | 
			
		||||
        var nbt = stack.getTag();
 | 
			
		||||
        return nbt != null && nbt.contains(NBT_INSTANCE) ? nbt.getInt(NBT_INSTANCE) : -1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void setInstanceID(ItemStack stack, int instanceID) {
 | 
			
		||||
        stack.getOrCreateTag().putInt(NBT_INSTANCE, instanceID);
 | 
			
		||||
        return nbt != null && nbt.hasUUID(NBT_INSTANCE) ? nbt.getUUID(NBT_INSTANCE) : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static int getSessionID(ItemStack stack) {
 | 
			
		||||
@@ -281,10 +278,6 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
 | 
			
		||||
        return nbt != null && nbt.contains(NBT_SESSION) ? nbt.getInt(NBT_SESSION) : -1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void setSessionID(ItemStack stack, int sessionID) {
 | 
			
		||||
        stack.getOrCreateTag().putInt(NBT_SESSION, sessionID);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static boolean isMarkedOn(ItemStack stack) {
 | 
			
		||||
        var nbt = stack.getTag();
 | 
			
		||||
        return nbt != null && nbt.getBoolean(NBT_ON);
 | 
			
		||||
 
 | 
			
		||||
@@ -25,10 +25,9 @@ import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.core.Direction;
 | 
			
		||||
import net.minecraft.core.NonNullList;
 | 
			
		||||
import net.minecraft.nbt.CompoundTag;
 | 
			
		||||
import net.minecraft.nbt.ListTag;
 | 
			
		||||
import net.minecraft.nbt.Tag;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
import net.minecraft.world.ContainerHelper;
 | 
			
		||||
import net.minecraft.world.entity.player.Inventory;
 | 
			
		||||
import net.minecraft.world.entity.player.Player;
 | 
			
		||||
import net.minecraft.world.inventory.AbstractContainerMenu;
 | 
			
		||||
@@ -136,17 +135,8 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba
 | 
			
		||||
        super.loadServer(nbt);
 | 
			
		||||
 | 
			
		||||
        // Read inventory
 | 
			
		||||
        var nbttaglist = nbt.getList("Items", Tag.TAG_COMPOUND);
 | 
			
		||||
        inventory.clear();
 | 
			
		||||
        inventorySnapshot.clear();
 | 
			
		||||
        for (var i = 0; i < nbttaglist.size(); i++) {
 | 
			
		||||
            var tag = nbttaglist.getCompound(i);
 | 
			
		||||
            var slot = tag.getByte("Slot") & 0xff;
 | 
			
		||||
            if (slot < getContainerSize()) {
 | 
			
		||||
                inventory.set(slot, ItemStack.of(tag));
 | 
			
		||||
                inventorySnapshot.set(slot, inventory.get(slot).copy());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        ContainerHelper.loadAllItems(nbt, inventory);
 | 
			
		||||
        for (var i = 0; i < inventory.size(); i++) inventorySnapshot.set(i, inventory.get(i).copy());
 | 
			
		||||
 | 
			
		||||
        // Read state
 | 
			
		||||
        brain.readFromNBT(nbt);
 | 
			
		||||
@@ -155,16 +145,7 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba
 | 
			
		||||
    @Override
 | 
			
		||||
    public void saveAdditional(CompoundTag nbt) {
 | 
			
		||||
        // Write inventory
 | 
			
		||||
        var nbttaglist = new ListTag();
 | 
			
		||||
        for (var i = 0; i < INVENTORY_SIZE; i++) {
 | 
			
		||||
            if (!inventory.get(i).isEmpty()) {
 | 
			
		||||
                var tag = new CompoundTag();
 | 
			
		||||
                tag.putByte("Slot", (byte) i);
 | 
			
		||||
                inventory.get(i).save(tag);
 | 
			
		||||
                nbttaglist.add(tag);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        nbt.put("Items", nbttaglist);
 | 
			
		||||
        ContainerHelper.saveAllItems(nbt, inventory);
 | 
			
		||||
 | 
			
		||||
        // Write brain
 | 
			
		||||
        nbt = brain.writeToNBT(nbt);
 | 
			
		||||
@@ -186,7 +167,7 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba
 | 
			
		||||
        if (dir.getAxis() == Direction.Axis.Y) dir = Direction.NORTH;
 | 
			
		||||
        level.setBlockAndUpdate(worldPosition, getBlockState().setValue(TurtleBlock.FACING, dir));
 | 
			
		||||
 | 
			
		||||
        updateOutput();
 | 
			
		||||
        updateRedstone();
 | 
			
		||||
        updateInputsImmediately();
 | 
			
		||||
 | 
			
		||||
        onTileEntityChange();
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ import dan200.computercraft.api.turtle.TurtleCommand;
 | 
			
		||||
import dan200.computercraft.api.turtle.TurtleSide;
 | 
			
		||||
import dan200.computercraft.api.upgrades.UpgradeData;
 | 
			
		||||
import dan200.computercraft.core.computer.ComputerSide;
 | 
			
		||||
import dan200.computercraft.core.util.PeripheralHelpers;
 | 
			
		||||
import dan200.computercraft.impl.TurtleUpgrades;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ServerComputer;
 | 
			
		||||
@@ -296,7 +297,7 @@ public class TurtleBrain implements TurtleAccessInternal {
 | 
			
		||||
                        oldWorld.removeBlock(oldPos, false);
 | 
			
		||||
 | 
			
		||||
                        // Make sure everybody knows about it
 | 
			
		||||
                        newTurtle.updateOutput();
 | 
			
		||||
                        newTurtle.updateRedstone();
 | 
			
		||||
                        newTurtle.updateInputsImmediately();
 | 
			
		||||
                        return true;
 | 
			
		||||
                    }
 | 
			
		||||
@@ -589,7 +590,7 @@ public class TurtleBrain implements TurtleAccessInternal {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var existing = peripherals.get(side);
 | 
			
		||||
            if (existing == peripheral || (existing != null && peripheral != null && existing.equals(peripheral))) {
 | 
			
		||||
            if (PeripheralHelpers.equals(existing, peripheral)) {
 | 
			
		||||
                // If the peripheral is the same, just use that.
 | 
			
		||||
                peripheral = existing;
 | 
			
		||||
            } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -54,7 +54,7 @@ public class TurtleSuckCommand implements TurtleCommand {
 | 
			
		||||
                case ContainerTransfer.NO_SPACE:
 | 
			
		||||
                    return TurtleCommandResult.failure("No space for items");
 | 
			
		||||
                case ContainerTransfer.NO_ITEMS:
 | 
			
		||||
                    return TurtleCommandResult.failure("No items to drop");
 | 
			
		||||
                    return TurtleCommandResult.failure("No items to take");
 | 
			
		||||
                default:
 | 
			
		||||
                    turtle.playAnimation(TurtleAnimation.WAIT);
 | 
			
		||||
                    return TurtleCommandResult.success();
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,11 @@ public final class DirectionUtil {
 | 
			
		||||
    private DirectionUtil() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A bitmask indicating all sides.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int ALL_SIDES = (1 << 6) - 1;
 | 
			
		||||
 | 
			
		||||
    public static final Direction[] FACINGS = Direction.values();
 | 
			
		||||
 | 
			
		||||
    public static ComputerSide toLocal(Direction front, Direction dir) {
 | 
			
		||||
@@ -31,4 +36,15 @@ public final class DirectionUtil {
 | 
			
		||||
            default -> 0.0f;
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Determine if a direction is in a bitmask.
 | 
			
		||||
     *
 | 
			
		||||
     * @param mask      The bitmask to test
 | 
			
		||||
     * @param direction The direction to check.
 | 
			
		||||
     * @return Whether the direction is in a bitmask.
 | 
			
		||||
     */
 | 
			
		||||
    public static boolean isSet(int mask, Direction direction) {
 | 
			
		||||
        return (mask & (1 << direction.ordinal())) != 0;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,13 +5,20 @@
 | 
			
		||||
package dan200.computercraft.shared.util;
 | 
			
		||||
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.resources.ResourceKey;
 | 
			
		||||
import net.minecraft.server.level.ChunkLevel;
 | 
			
		||||
import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
import net.minecraft.world.level.ChunkPos;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
import net.minecraft.world.level.LevelAccessor;
 | 
			
		||||
import net.minecraft.world.level.block.Block;
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntity;
 | 
			
		||||
import net.minecraft.world.level.chunk.ChunkStatus;
 | 
			
		||||
import net.minecraft.world.level.chunk.LevelChunk;
 | 
			
		||||
 | 
			
		||||
import java.util.Queue;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
import java.util.concurrent.ConcurrentLinkedDeque;
 | 
			
		||||
import java.util.concurrent.atomic.AtomicBoolean;
 | 
			
		||||
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A thread-safe version of {@link LevelAccessor#scheduleTick(BlockPos, Block, int)}.
 | 
			
		||||
@@ -22,26 +29,92 @@ public final class TickScheduler {
 | 
			
		||||
    private TickScheduler() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The list of block entities to tick.
 | 
			
		||||
     */
 | 
			
		||||
    private static final Queue<Token> toTick = new ConcurrentLinkedDeque<>();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Block entities which we want to tick, but whose chunks not currently loaded.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * Minecraft sometimes keeps chunks in-memory, but not actively loaded. If such a block entity is in the
 | 
			
		||||
     * {@link #toTick} queue, we'll see that it's not loaded and so have to skip scheduling a tick.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * However, if the block entity is ever loaded again, we need to tick it. Unfortunately, block entities in this
 | 
			
		||||
     * state are not notified in any way (for instance, {@link BlockEntity#setRemoved()} or
 | 
			
		||||
     * {@link BlockEntity#clearRemoved()} are not called), and so there's no way to easily reschedule them for ticking.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * Instead, for each chunk we keep a list of all block entities whose tick we skipped. If a chunk is loaded,
 | 
			
		||||
     * {@linkplain #onChunkTicketChanged(ServerLevel, long, int, int) we requeue all skipped ticks}.
 | 
			
		||||
     */
 | 
			
		||||
    private static final Map<ChunkReference, List<Token>> delayed = new HashMap<>();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Schedule a block entity to be ticked.
 | 
			
		||||
     *
 | 
			
		||||
     * @param token The token whose block entity should be ticked.
 | 
			
		||||
     */
 | 
			
		||||
    public static void schedule(Token token) {
 | 
			
		||||
        var world = token.owner.getLevel();
 | 
			
		||||
        if (world != null && !world.isClientSide && !token.scheduled.getAndSet(true)) toTick.add(token);
 | 
			
		||||
        if (world != null && !world.isClientSide && Token.STATE.compareAndSet(token, State.IDLE, State.SCHEDULED)) {
 | 
			
		||||
            toTick.add(token);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void onChunkTicketChanged(ServerLevel level, long chunkPos, int oldLevel, int newLevel) {
 | 
			
		||||
        boolean oldLoaded = isLoaded(oldLevel), newLoaded = isLoaded(newLevel);
 | 
			
		||||
        if (!oldLoaded && newLoaded) {
 | 
			
		||||
            // If our chunk is becoming active, requeue all pending tokens.
 | 
			
		||||
            var delayedTokens = delayed.remove(new ChunkReference(level.dimension(), chunkPos));
 | 
			
		||||
            if (delayedTokens == null) return;
 | 
			
		||||
 | 
			
		||||
            for (var token : delayedTokens) {
 | 
			
		||||
                if (token.owner.isRemoved()) {
 | 
			
		||||
                    Token.STATE.set(token, State.IDLE);
 | 
			
		||||
                } else {
 | 
			
		||||
                    Token.STATE.set(token, State.SCHEDULED);
 | 
			
		||||
                    toTick.add(token);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void onChunkUnload(LevelChunk chunk) {
 | 
			
		||||
        // If our chunk is fully unloaded, all block entities are about to be removed - we need to dequeue any delayed
 | 
			
		||||
        // tokens from the queue.
 | 
			
		||||
        var delayedTokens = delayed.remove(new ChunkReference(chunk.getLevel().dimension(), chunk.getPos().toLong()));
 | 
			
		||||
        if (delayedTokens == null) return;
 | 
			
		||||
 | 
			
		||||
        for (var token : delayedTokens) Token.STATE.set(token, State.IDLE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void tick() {
 | 
			
		||||
        Token token;
 | 
			
		||||
        while ((token = toTick.poll()) != null) {
 | 
			
		||||
            token.scheduled.set(false);
 | 
			
		||||
            var blockEntity = token.owner;
 | 
			
		||||
            if (blockEntity.isRemoved()) continue;
 | 
			
		||||
        while ((token = toTick.poll()) != null) Token.STATE.set(token, tickToken(token));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
            var world = blockEntity.getLevel();
 | 
			
		||||
    private static State tickToken(Token token) {
 | 
			
		||||
        var blockEntity = token.owner;
 | 
			
		||||
 | 
			
		||||
        // If the block entity has been removed, then remove it from the queue.
 | 
			
		||||
        if (blockEntity.isRemoved()) return State.IDLE;
 | 
			
		||||
 | 
			
		||||
        var level = Objects.requireNonNull(blockEntity.getLevel(), "Block entity level cannot become null");
 | 
			
		||||
        var pos = blockEntity.getBlockPos();
 | 
			
		||||
 | 
			
		||||
            if (world != null && world.isLoaded(pos) && world.getBlockEntity(pos) == blockEntity) {
 | 
			
		||||
                world.scheduleTick(pos, blockEntity.getBlockState().getBlock(), 0);
 | 
			
		||||
        if (!level.isLoaded(pos)) {
 | 
			
		||||
            // The chunk is not properly loaded, as it to our delayed set.
 | 
			
		||||
            delayed.computeIfAbsent(new ChunkReference(level.dimension(), ChunkPos.asLong(pos)), x -> new ArrayList<>()).add(token);
 | 
			
		||||
            return State.UNLOADED;
 | 
			
		||||
        } else {
 | 
			
		||||
            // This should be impossible: either the block entity is at the above position, or it has been removed.
 | 
			
		||||
            if (level.getBlockEntity(pos) != blockEntity) {
 | 
			
		||||
                throw new IllegalStateException("Expected " + blockEntity + " at " + pos);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Otherwise schedule a tick and remove it from the queue.
 | 
			
		||||
            level.scheduleTick(pos, blockEntity.getBlockState().getBlock(), 0);
 | 
			
		||||
            return State.IDLE;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -52,11 +125,51 @@ public final class TickScheduler {
 | 
			
		||||
     * As such, it should be unique per {@link BlockEntity} instance to avoid it being queued multiple times.
 | 
			
		||||
     */
 | 
			
		||||
    public static class Token {
 | 
			
		||||
        static final AtomicReferenceFieldUpdater<Token, State> STATE = AtomicReferenceFieldUpdater.newUpdater(Token.class, State.class, "$state");
 | 
			
		||||
 | 
			
		||||
        final BlockEntity owner;
 | 
			
		||||
        final AtomicBoolean scheduled = new AtomicBoolean();
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * The current state of this token.
 | 
			
		||||
         */
 | 
			
		||||
        private volatile State $state = State.IDLE;
 | 
			
		||||
 | 
			
		||||
        public Token(BlockEntity owner) {
 | 
			
		||||
            this.owner = owner;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The possible states a {@link Token} can be in.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * This effectively stores which (if any) queue the token is currently in, allowing us to skip scheduling if the
 | 
			
		||||
     * token is already enqueued.
 | 
			
		||||
     */
 | 
			
		||||
    private enum State {
 | 
			
		||||
        /**
 | 
			
		||||
         * The token is not on any queues.
 | 
			
		||||
         */
 | 
			
		||||
        IDLE,
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * The token is on the {@link #toTick} queue.
 | 
			
		||||
         */
 | 
			
		||||
        SCHEDULED,
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * The token is on the {@link #delayed} queue.
 | 
			
		||||
         */
 | 
			
		||||
        UNLOADED,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private record ChunkReference(ResourceKey<Level> level, Long position) {
 | 
			
		||||
        @Override
 | 
			
		||||
        public String toString() {
 | 
			
		||||
            return "ChunkReference(" + level + " at " + new ChunkPos(position) + ")";
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static boolean isLoaded(int level) {
 | 
			
		||||
        return level <= ChunkLevel.byStatus(ChunkStatus.FULL);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ accessWidener v1 named
 | 
			
		||||
# Additional access wideners for vanilla code. This is a effectively the subset of Fabric's transitive access wideners
 | 
			
		||||
# that we actually use
 | 
			
		||||
 | 
			
		||||
accessible method net/minecraft/client/renderer/item/ItemProperties register (Lnet/minecraft/world/item/Item;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/renderer/item/ClampedItemPropertyFunction;)V
 | 
			
		||||
accessible method net/minecraft/client/renderer/blockentity/BlockEntityRenderers register (Lnet/minecraft/world/level/block/entity/BlockEntityType;Lnet/minecraft/client/renderer/blockentity/BlockEntityRendererProvider;)V
 | 
			
		||||
accessible class net/minecraft/world/item/CreativeModeTab$Output
 | 
			
		||||
accessible field net/minecraft/world/item/CreativeModeTabs OP_BLOCKS Lnet/minecraft/resources/ResourceKey;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.sound;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
@@ -16,7 +17,7 @@ public class DfpwmStreamTest {
 | 
			
		||||
        var stream = new DfpwmStream();
 | 
			
		||||
 | 
			
		||||
        var input = ByteBuffer.wrap(new byte[]{ 43, -31, 33, 44, 30, -16, -85, 23, -3, -55, 46, -70, 68, -67, 74, -96, -68, 16, 94, -87, -5, 87, 11, -16, 19, 92, 85, -71, 126, 5, -84, 64, 17, -6, 85, -11, -1, -87, -12, 1, 85, -56, 33, -80, 82, 104, -93, 17, 126, 23, 91, -30, 37, -32, 117, -72, -58, 11, -76, 19, -108, 86, -65, -10, -1, -68, -25, 10, -46, 85, 124, -54, 15, -24, 43, -94, 117, 63, -36, 15, -6, 88, 87, -26, -83, 106, 41, 13, -28, -113, -10, -66, 119, -87, -113, 68, -55, 40, -107, 62, 20, 72, 3, -96, 114, -87, -2, 39, -104, 30, 20, 42, 84, 24, 47, 64, 43, 61, -35, 95, -65, 42, 61, 42, -50, 4, -9, 81 });
 | 
			
		||||
        stream.push(input);
 | 
			
		||||
        stream.push(new EncodedAudio(0, 0, false, input));
 | 
			
		||||
 | 
			
		||||
        var buffer = stream.read(1024 + 1);
 | 
			
		||||
        assertEquals(1024, buffer.remaining(), "Must have read 1024 bytes");
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,183 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.impl.network.wired;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.network.wired.WiredNetwork;
 | 
			
		||||
import dan200.computercraft.impl.network.wired.NetworkTest.NetworkElement;
 | 
			
		||||
import dan200.computercraft.shared.util.DirectionUtil;
 | 
			
		||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
 | 
			
		||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import org.openjdk.jmh.annotations.*;
 | 
			
		||||
import org.openjdk.jmh.runner.Runner;
 | 
			
		||||
import org.openjdk.jmh.runner.RunnerException;
 | 
			
		||||
import org.openjdk.jmh.runner.options.OptionsBuilder;
 | 
			
		||||
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
import java.util.function.BiConsumer;
 | 
			
		||||
import java.util.function.BiFunction;
 | 
			
		||||
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertEquals;
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
 | 
			
		||||
 | 
			
		||||
@BenchmarkMode(Mode.AverageTime)
 | 
			
		||||
@OutputTimeUnit(TimeUnit.MILLISECONDS)
 | 
			
		||||
public class NetworkBenchmark {
 | 
			
		||||
    private static final int BRUTE_SIZE = 16;
 | 
			
		||||
 | 
			
		||||
    public static void main(String[] args) throws RunnerException {
 | 
			
		||||
        var opts = new OptionsBuilder()
 | 
			
		||||
            .include(NetworkBenchmark.class.getName() + "\\..*")
 | 
			
		||||
            .warmupIterations(2)
 | 
			
		||||
            .measurementIterations(5)
 | 
			
		||||
            .forks(1)
 | 
			
		||||
            .build();
 | 
			
		||||
        new Runner(opts).run();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Benchmark
 | 
			
		||||
    @Warmup(time = 1, timeUnit = TimeUnit.SECONDS)
 | 
			
		||||
    @Measurement(time = 2, timeUnit = TimeUnit.SECONDS)
 | 
			
		||||
    public void removeEveryNode(ConnectedGrid grid) {
 | 
			
		||||
        grid.grid.forEach((node, pos) -> node.remove());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Benchmark
 | 
			
		||||
    @Warmup(time = 1, timeUnit = TimeUnit.SECONDS)
 | 
			
		||||
    @Measurement(time = 2, timeUnit = TimeUnit.SECONDS)
 | 
			
		||||
    public void connectAndDisconnect(SplitGrid connectedGrid) {
 | 
			
		||||
        WiredNodeImpl left = connectedGrid.left, right = connectedGrid.right;
 | 
			
		||||
 | 
			
		||||
        assertNotEquals(left.getNetwork(), right.getNetwork());
 | 
			
		||||
        left.connectTo(right);
 | 
			
		||||
        assertEquals(left.getNetwork(), right.getNetwork());
 | 
			
		||||
        left.disconnectFrom(right);
 | 
			
		||||
        assertNotEquals(left.getNetwork(), right.getNetwork());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Benchmark
 | 
			
		||||
    @Warmup(time = 1, timeUnit = TimeUnit.SECONDS)
 | 
			
		||||
    @Measurement(time = 2, timeUnit = TimeUnit.SECONDS)
 | 
			
		||||
    public void connectAndRemove(SplitGrid connectedGrid) {
 | 
			
		||||
        WiredNodeImpl left = connectedGrid.left, right = connectedGrid.right, centre = connectedGrid.centre;
 | 
			
		||||
 | 
			
		||||
        assertNotEquals(left.getNetwork(), right.getNetwork());
 | 
			
		||||
        centre.connectTo(left);
 | 
			
		||||
        centre.connectTo(right);
 | 
			
		||||
        assertEquals(left.getNetwork(), right.getNetwork());
 | 
			
		||||
        centre.remove();
 | 
			
		||||
        assertNotEquals(left.getNetwork(), right.getNetwork());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a grid where all nodes are connected to their neighbours.
 | 
			
		||||
     */
 | 
			
		||||
    @State(Scope.Thread)
 | 
			
		||||
    public static class ConnectedGrid {
 | 
			
		||||
        Grid<WiredNodeImpl> grid;
 | 
			
		||||
 | 
			
		||||
        @Setup(Level.Invocation)
 | 
			
		||||
        public void setup() {
 | 
			
		||||
            var grid = this.grid = new Grid<>(BRUTE_SIZE);
 | 
			
		||||
            grid.map((existing, pos) -> new NetworkElement("n_" + pos, pos.getX() == pos.getY() && pos.getY() == pos.getZ()).getNode());
 | 
			
		||||
 | 
			
		||||
            // Connect every node
 | 
			
		||||
            grid.forEach((node, pos) -> {
 | 
			
		||||
                for (var facing : DirectionUtil.FACINGS) {
 | 
			
		||||
                    var other = grid.get(pos.relative(facing));
 | 
			
		||||
                    if (other != null) node.connectTo(other);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            var networks = countNetworks(grid);
 | 
			
		||||
            if (networks.size() != 1) throw new AssertionError("Expected exactly one network.");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a grid where the nodes at {@code x < BRUTE_SIZE/2} and {@code x >= BRUTE_SIZE/2} are in separate networks,
 | 
			
		||||
     * but otherwise connected to their neighbours.
 | 
			
		||||
     */
 | 
			
		||||
    @State(Scope.Thread)
 | 
			
		||||
    public static class SplitGrid {
 | 
			
		||||
        Grid<WiredNodeImpl> grid;
 | 
			
		||||
        WiredNodeImpl left, right, centre;
 | 
			
		||||
 | 
			
		||||
        @Setup
 | 
			
		||||
        public void setup() {
 | 
			
		||||
            var grid = this.grid = new Grid<>(BRUTE_SIZE);
 | 
			
		||||
            grid.map((existing, pos) -> new NetworkElement("n_" + pos, pos.getX() == pos.getY() && pos.getY() == pos.getZ()).getNode());
 | 
			
		||||
 | 
			
		||||
            // Connect every node
 | 
			
		||||
            grid.forEach((node, pos) -> {
 | 
			
		||||
                for (var facing : DirectionUtil.FACINGS) {
 | 
			
		||||
                    var offset = pos.relative(facing);
 | 
			
		||||
                    if (offset.getX() >= BRUTE_SIZE / 2 == pos.getX() >= BRUTE_SIZE / 2) {
 | 
			
		||||
                        var other = grid.get(offset);
 | 
			
		||||
                        if (other != null) node.connectTo(other);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            var networks = countNetworks(grid);
 | 
			
		||||
            if (networks.size() != 2) throw new AssertionError("Expected exactly two networks.");
 | 
			
		||||
            for (var network : networks.object2IntEntrySet()) {
 | 
			
		||||
                if (network.getIntValue() != BRUTE_SIZE * BRUTE_SIZE * (BRUTE_SIZE / 2)) {
 | 
			
		||||
                    throw new AssertionError("Network is the wrong size");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            left = Objects.requireNonNull(grid.get(new BlockPos(BRUTE_SIZE / 2 - 1, 0, 0)));
 | 
			
		||||
            right = Objects.requireNonNull(grid.get(new BlockPos(BRUTE_SIZE / 2, 0, 0)));
 | 
			
		||||
            centre = new NetworkElement("c", false).getNode();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static Object2IntMap<WiredNetwork> countNetworks(Grid<WiredNodeImpl> grid) {
 | 
			
		||||
        Object2IntMap<WiredNetwork> networks = new Object2IntOpenHashMap<>();
 | 
			
		||||
        grid.forEach((node, pos) -> networks.put(node.network, networks.getOrDefault(node.network, 0) + 1));
 | 
			
		||||
        return networks;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static class Grid<T> {
 | 
			
		||||
        private final int size;
 | 
			
		||||
        private final T[] box;
 | 
			
		||||
 | 
			
		||||
        @SuppressWarnings("unchecked")
 | 
			
		||||
        Grid(int size) {
 | 
			
		||||
            this.size = size;
 | 
			
		||||
            this.box = (T[]) new Object[size * size * size];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public T get(BlockPos pos) {
 | 
			
		||||
            int x = pos.getX(), y = pos.getY(), z = pos.getZ();
 | 
			
		||||
 | 
			
		||||
            return x >= 0 && x < size && y >= 0 && y < size && z >= 0 && z < size
 | 
			
		||||
                ? box[x * size * size + y * size + z]
 | 
			
		||||
                : null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void forEach(BiConsumer<T, BlockPos> transform) {
 | 
			
		||||
            for (var x = 0; x < size; x++) {
 | 
			
		||||
                for (var y = 0; y < size; y++) {
 | 
			
		||||
                    for (var z = 0; z < size; z++) {
 | 
			
		||||
                        transform.accept(box[x * size * size + y * size + z], new BlockPos(x, y, z));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void map(BiFunction<T, BlockPos, T> transform) {
 | 
			
		||||
            for (var x = 0; x < size; x++) {
 | 
			
		||||
                for (var y = 0; y < size; y++) {
 | 
			
		||||
                    for (var z = 0; z < size; z++) {
 | 
			
		||||
                        box[x * size * size + y * size + z] = transform.apply(box[x * size * size + y * size + z], new BlockPos(x, y, z));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -9,19 +9,14 @@ import dan200.computercraft.api.network.wired.WiredNetwork;
 | 
			
		||||
import dan200.computercraft.api.network.wired.WiredNetworkChange;
 | 
			
		||||
import dan200.computercraft.api.network.wired.WiredNode;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import dan200.computercraft.shared.util.DirectionUtil;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
import net.minecraft.world.phys.Vec3;
 | 
			
		||||
import org.junit.jupiter.api.Disabled;
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
import java.util.function.BiConsumer;
 | 
			
		||||
import java.util.function.BiFunction;
 | 
			
		||||
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.*;
 | 
			
		||||
 | 
			
		||||
@@ -29,11 +24,11 @@ public class NetworkTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testConnect() {
 | 
			
		||||
        NetworkElement
 | 
			
		||||
            aE = new NetworkElement(null, null, "a"),
 | 
			
		||||
            bE = new NetworkElement(null, null, "b"),
 | 
			
		||||
            cE = new NetworkElement(null, null, "c");
 | 
			
		||||
            aE = new NetworkElement("a"),
 | 
			
		||||
            bE = new NetworkElement("b"),
 | 
			
		||||
            cE = new NetworkElement("c");
 | 
			
		||||
 | 
			
		||||
        WiredNode
 | 
			
		||||
        WiredNodeImpl
 | 
			
		||||
            aN = aE.getNode(),
 | 
			
		||||
            bN = bE.getNode(),
 | 
			
		||||
            cN = cE.getNode();
 | 
			
		||||
@@ -42,8 +37,8 @@ public class NetworkTest {
 | 
			
		||||
        assertNotEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be different");
 | 
			
		||||
        assertNotEquals(bN.getNetwork(), cN.getNetwork(), "B's and C's network must be different");
 | 
			
		||||
 | 
			
		||||
        assertTrue(aN.getNetwork().connect(aN, bN), "Must be able to add connection");
 | 
			
		||||
        assertFalse(aN.getNetwork().connect(aN, bN), "Cannot add connection twice");
 | 
			
		||||
        assertTrue(aN.connectTo(bN), "Must be able to add connection");
 | 
			
		||||
        assertFalse(aN.connectTo(bN), "Cannot add connection twice");
 | 
			
		||||
 | 
			
		||||
        assertEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must be equal");
 | 
			
		||||
        assertEquals(Set.of(aN, bN), nodes(aN.getNetwork()), "A's network should be A and B");
 | 
			
		||||
@@ -51,7 +46,7 @@ public class NetworkTest {
 | 
			
		||||
        assertEquals(Set.of("a", "b"), aE.allPeripherals().keySet(), "A's peripheral set should be A, B");
 | 
			
		||||
        assertEquals(Set.of("a", "b"), bE.allPeripherals().keySet(), "B's peripheral set should be A, B");
 | 
			
		||||
 | 
			
		||||
        aN.getNetwork().connect(aN, cN);
 | 
			
		||||
        aN.connectTo(cN);
 | 
			
		||||
 | 
			
		||||
        assertEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must be equal");
 | 
			
		||||
        assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal");
 | 
			
		||||
@@ -69,20 +64,20 @@ public class NetworkTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testDisconnectNoChange() {
 | 
			
		||||
        NetworkElement
 | 
			
		||||
            aE = new NetworkElement(null, null, "a"),
 | 
			
		||||
            bE = new NetworkElement(null, null, "b"),
 | 
			
		||||
            cE = new NetworkElement(null, null, "c");
 | 
			
		||||
            aE = new NetworkElement("a"),
 | 
			
		||||
            bE = new NetworkElement("b"),
 | 
			
		||||
            cE = new NetworkElement("c");
 | 
			
		||||
 | 
			
		||||
        WiredNode
 | 
			
		||||
        WiredNodeImpl
 | 
			
		||||
            aN = aE.getNode(),
 | 
			
		||||
            bN = bE.getNode(),
 | 
			
		||||
            cN = cE.getNode();
 | 
			
		||||
 | 
			
		||||
        aN.getNetwork().connect(aN, bN);
 | 
			
		||||
        aN.getNetwork().connect(aN, cN);
 | 
			
		||||
        aN.getNetwork().connect(bN, cN);
 | 
			
		||||
        aN.connectTo(bN);
 | 
			
		||||
        aN.connectTo(cN);
 | 
			
		||||
        bN.connectTo(cN);
 | 
			
		||||
 | 
			
		||||
        aN.getNetwork().disconnect(aN, bN);
 | 
			
		||||
        aN.disconnectFrom(bN);
 | 
			
		||||
 | 
			
		||||
        assertEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must be equal");
 | 
			
		||||
        assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal");
 | 
			
		||||
@@ -96,19 +91,19 @@ public class NetworkTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testDisconnectLeaf() {
 | 
			
		||||
        NetworkElement
 | 
			
		||||
            aE = new NetworkElement(null, null, "a"),
 | 
			
		||||
            bE = new NetworkElement(null, null, "b"),
 | 
			
		||||
            cE = new NetworkElement(null, null, "c");
 | 
			
		||||
            aE = new NetworkElement("a"),
 | 
			
		||||
            bE = new NetworkElement("b"),
 | 
			
		||||
            cE = new NetworkElement("c");
 | 
			
		||||
 | 
			
		||||
        WiredNode
 | 
			
		||||
        WiredNodeImpl
 | 
			
		||||
            aN = aE.getNode(),
 | 
			
		||||
            bN = bE.getNode(),
 | 
			
		||||
            cN = cE.getNode();
 | 
			
		||||
 | 
			
		||||
        aN.getNetwork().connect(aN, bN);
 | 
			
		||||
        aN.getNetwork().connect(aN, cN);
 | 
			
		||||
        aN.connectTo(bN);
 | 
			
		||||
        aN.connectTo(cN);
 | 
			
		||||
 | 
			
		||||
        aN.getNetwork().disconnect(aN, bN);
 | 
			
		||||
        aN.disconnectFrom(bN);
 | 
			
		||||
 | 
			
		||||
        assertNotEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal");
 | 
			
		||||
        assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal");
 | 
			
		||||
@@ -123,23 +118,23 @@ public class NetworkTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testDisconnectSplit() {
 | 
			
		||||
        NetworkElement
 | 
			
		||||
            aE = new NetworkElement(null, null, "a"),
 | 
			
		||||
            aaE = new NetworkElement(null, null, "a_"),
 | 
			
		||||
            bE = new NetworkElement(null, null, "b"),
 | 
			
		||||
            bbE = new NetworkElement(null, null, "b_");
 | 
			
		||||
            aE = new NetworkElement("a"),
 | 
			
		||||
            aaE = new NetworkElement("a_"),
 | 
			
		||||
            bE = new NetworkElement("b"),
 | 
			
		||||
            bbE = new NetworkElement("b_");
 | 
			
		||||
 | 
			
		||||
        WiredNode
 | 
			
		||||
        WiredNodeImpl
 | 
			
		||||
            aN = aE.getNode(),
 | 
			
		||||
            aaN = aaE.getNode(),
 | 
			
		||||
            bN = bE.getNode(),
 | 
			
		||||
            bbN = bbE.getNode();
 | 
			
		||||
 | 
			
		||||
        aN.getNetwork().connect(aN, aaN);
 | 
			
		||||
        bN.getNetwork().connect(bN, bbN);
 | 
			
		||||
        aN.connectTo(aaN);
 | 
			
		||||
        bN.connectTo(bbN);
 | 
			
		||||
 | 
			
		||||
        aN.getNetwork().connect(aN, bN);
 | 
			
		||||
        aN.connectTo(bN);
 | 
			
		||||
 | 
			
		||||
        aN.getNetwork().disconnect(aN, bN);
 | 
			
		||||
        aN.disconnectFrom(bN);
 | 
			
		||||
 | 
			
		||||
        assertNotEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal");
 | 
			
		||||
        assertEquals(aN.getNetwork(), aaN.getNetwork(), "A's and A_'s network must be equal");
 | 
			
		||||
@@ -154,7 +149,7 @@ public class NetworkTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testRemoveSingle() {
 | 
			
		||||
        var aE = new NetworkElement(null, null, "a");
 | 
			
		||||
        var aE = new NetworkElement("a");
 | 
			
		||||
        var aN = aE.getNode();
 | 
			
		||||
 | 
			
		||||
        var network = aN.getNetwork();
 | 
			
		||||
@@ -165,20 +160,20 @@ public class NetworkTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testRemoveLeaf() {
 | 
			
		||||
        NetworkElement
 | 
			
		||||
            aE = new NetworkElement(null, null, "a"),
 | 
			
		||||
            bE = new NetworkElement(null, null, "b"),
 | 
			
		||||
            cE = new NetworkElement(null, null, "c");
 | 
			
		||||
            aE = new NetworkElement("a"),
 | 
			
		||||
            bE = new NetworkElement("b"),
 | 
			
		||||
            cE = new NetworkElement("c");
 | 
			
		||||
 | 
			
		||||
        WiredNode
 | 
			
		||||
        WiredNodeImpl
 | 
			
		||||
            aN = aE.getNode(),
 | 
			
		||||
            bN = bE.getNode(),
 | 
			
		||||
            cN = cE.getNode();
 | 
			
		||||
 | 
			
		||||
        aN.getNetwork().connect(aN, bN);
 | 
			
		||||
        aN.getNetwork().connect(aN, cN);
 | 
			
		||||
        aN.connectTo(bN);
 | 
			
		||||
        aN.connectTo(cN);
 | 
			
		||||
 | 
			
		||||
        assertTrue(aN.getNetwork().remove(bN), "Must be able to remove node");
 | 
			
		||||
        assertFalse(aN.getNetwork().remove(bN), "Cannot remove a second time");
 | 
			
		||||
        assertTrue(bN.remove(), "Must be able to remove node");
 | 
			
		||||
        assertFalse(bN.remove(), "Cannot remove a second time");
 | 
			
		||||
 | 
			
		||||
        assertNotEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal");
 | 
			
		||||
        assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal");
 | 
			
		||||
@@ -194,26 +189,26 @@ public class NetworkTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testRemoveSplit() {
 | 
			
		||||
        NetworkElement
 | 
			
		||||
            aE = new NetworkElement(null, null, "a"),
 | 
			
		||||
            aaE = new NetworkElement(null, null, "a_"),
 | 
			
		||||
            bE = new NetworkElement(null, null, "b"),
 | 
			
		||||
            bbE = new NetworkElement(null, null, "b_"),
 | 
			
		||||
            cE = new NetworkElement(null, null, "c");
 | 
			
		||||
            aE = new NetworkElement("a"),
 | 
			
		||||
            aaE = new NetworkElement("a_"),
 | 
			
		||||
            bE = new NetworkElement("b"),
 | 
			
		||||
            bbE = new NetworkElement("b_"),
 | 
			
		||||
            cE = new NetworkElement("c");
 | 
			
		||||
 | 
			
		||||
        WiredNode
 | 
			
		||||
        WiredNodeImpl
 | 
			
		||||
            aN = aE.getNode(),
 | 
			
		||||
            aaN = aaE.getNode(),
 | 
			
		||||
            bN = bE.getNode(),
 | 
			
		||||
            bbN = bbE.getNode(),
 | 
			
		||||
            cN = cE.getNode();
 | 
			
		||||
 | 
			
		||||
        aN.getNetwork().connect(aN, aaN);
 | 
			
		||||
        bN.getNetwork().connect(bN, bbN);
 | 
			
		||||
        aN.connectTo(aaN);
 | 
			
		||||
        bN.connectTo(bbN);
 | 
			
		||||
 | 
			
		||||
        cN.getNetwork().connect(aN, cN);
 | 
			
		||||
        cN.getNetwork().connect(bN, cN);
 | 
			
		||||
        cN.connectTo(aN);
 | 
			
		||||
        cN.connectTo(bN);
 | 
			
		||||
 | 
			
		||||
        cN.getNetwork().remove(cN);
 | 
			
		||||
        cN.remove();
 | 
			
		||||
 | 
			
		||||
        assertNotEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal");
 | 
			
		||||
        assertEquals(aN.getNetwork(), aaN.getNetwork(), "A's and A_'s network must be equal");
 | 
			
		||||
@@ -228,96 +223,30 @@ public class NetworkTest {
 | 
			
		||||
        assertEquals(Set.of(), cE.allPeripherals().keySet(), "C's peripheral set should be empty");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static final int BRUTE_SIZE = 16;
 | 
			
		||||
    private static final int TOGGLE_CONNECTION_TIMES = 5;
 | 
			
		||||
    private static final int TOGGLE_NODE_TIMES = 5;
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @Disabled("Takes a long time to run, mostly for stress testing")
 | 
			
		||||
    public void testLarge() {
 | 
			
		||||
        var grid = new Grid<WiredNode>(BRUTE_SIZE);
 | 
			
		||||
        grid.map((existing, pos) -> new NetworkElement(null, null, "n_" + pos).getNode());
 | 
			
		||||
 | 
			
		||||
        // Test connecting
 | 
			
		||||
        {
 | 
			
		||||
            var start = System.nanoTime();
 | 
			
		||||
 | 
			
		||||
            grid.forEach((existing, pos) -> {
 | 
			
		||||
                for (var facing : DirectionUtil.FACINGS) {
 | 
			
		||||
                    var offset = pos.relative(facing);
 | 
			
		||||
                    if (offset.getX() > BRUTE_SIZE / 2 == pos.getX() > BRUTE_SIZE / 2) {
 | 
			
		||||
                        var other = grid.get(offset);
 | 
			
		||||
                        if (other != null) existing.getNetwork().connect(existing, other);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            var end = System.nanoTime();
 | 
			
		||||
 | 
			
		||||
            System.out.printf("Connecting %s³ nodes took %s seconds\n", BRUTE_SIZE, (end - start) * 1e-9);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Test toggling
 | 
			
		||||
        {
 | 
			
		||||
            var left = grid.get(new BlockPos(BRUTE_SIZE / 2, 0, 0));
 | 
			
		||||
            var right = grid.get(new BlockPos(BRUTE_SIZE / 2 + 1, 0, 0));
 | 
			
		||||
            assertNotEquals(left.getNetwork(), right.getNetwork());
 | 
			
		||||
 | 
			
		||||
            var start = System.nanoTime();
 | 
			
		||||
            for (var i = 0; i < TOGGLE_CONNECTION_TIMES; i++) {
 | 
			
		||||
                left.getNetwork().connect(left, right);
 | 
			
		||||
                left.getNetwork().disconnect(left, right);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var end = System.nanoTime();
 | 
			
		||||
 | 
			
		||||
            System.out.printf("Toggling connection %s times took %s seconds\n", TOGGLE_CONNECTION_TIMES, (end - start) * 1e-9);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
            var left = grid.get(new BlockPos(BRUTE_SIZE / 2, 0, 0));
 | 
			
		||||
            var right = grid.get(new BlockPos(BRUTE_SIZE / 2 + 1, 0, 0));
 | 
			
		||||
            var centre = new NetworkElement(null, null, "c").getNode();
 | 
			
		||||
            assertNotEquals(left.getNetwork(), right.getNetwork());
 | 
			
		||||
 | 
			
		||||
            var start = System.nanoTime();
 | 
			
		||||
            for (var i = 0; i < TOGGLE_NODE_TIMES; i++) {
 | 
			
		||||
                left.getNetwork().connect(left, centre);
 | 
			
		||||
                right.getNetwork().connect(right, centre);
 | 
			
		||||
 | 
			
		||||
                left.getNetwork().remove(centre);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var end = System.nanoTime();
 | 
			
		||||
 | 
			
		||||
            System.out.printf("Toggling node %s times took %s seconds\n", TOGGLE_NODE_TIMES, (end - start) * 1e-9);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static final class NetworkElement implements WiredElement {
 | 
			
		||||
        private final Level world;
 | 
			
		||||
        private final Vec3 position;
 | 
			
		||||
    static final class NetworkElement implements WiredElement {
 | 
			
		||||
        private final String id;
 | 
			
		||||
        private final WiredNode node;
 | 
			
		||||
        private final WiredNodeImpl node;
 | 
			
		||||
        private final Map<String, IPeripheral> localPeripherals = new HashMap<>();
 | 
			
		||||
        private final Map<String, IPeripheral> remotePeripherals = new HashMap<>();
 | 
			
		||||
 | 
			
		||||
        private NetworkElement(Level world, Vec3 position, String id) {
 | 
			
		||||
            this.world = world;
 | 
			
		||||
            this.position = position;
 | 
			
		||||
        NetworkElement(String id) {
 | 
			
		||||
            this(id, true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        NetworkElement(String id, boolean peripheral) {
 | 
			
		||||
            this.id = id;
 | 
			
		||||
            this.node = new WiredNodeImpl(this);
 | 
			
		||||
            this.addPeripheral(id);
 | 
			
		||||
            if (peripheral) addPeripheral(id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public Level getLevel() {
 | 
			
		||||
            return world;
 | 
			
		||||
            throw new IllegalStateException("Unexpected call to getLevel()");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public Vec3 getPosition() {
 | 
			
		||||
            return position;
 | 
			
		||||
            throw new IllegalStateException("Unexpected call to getPosition()");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
@@ -331,7 +260,7 @@ public class NetworkTest {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public WiredNode getNode() {
 | 
			
		||||
        public WiredNodeImpl getNode() {
 | 
			
		||||
            return node;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -364,45 +293,6 @@ public class NetworkTest {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static class Grid<T> {
 | 
			
		||||
        private final int size;
 | 
			
		||||
        private final T[] box;
 | 
			
		||||
 | 
			
		||||
        @SuppressWarnings("unchecked")
 | 
			
		||||
        Grid(int size) {
 | 
			
		||||
            this.size = size;
 | 
			
		||||
            this.box = (T[]) new Object[size * size * size];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public T get(BlockPos pos) {
 | 
			
		||||
            int x = pos.getX(), y = pos.getY(), z = pos.getZ();
 | 
			
		||||
 | 
			
		||||
            return x >= 0 && x < size && y >= 0 && y < size && z >= 0 && z < size
 | 
			
		||||
                ? box[x * size * size + y * size + z]
 | 
			
		||||
                : null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void forEach(BiConsumer<T, BlockPos> transform) {
 | 
			
		||||
            for (var x = 0; x < size; x++) {
 | 
			
		||||
                for (var y = 0; y < size; y++) {
 | 
			
		||||
                    for (var z = 0; z < size; z++) {
 | 
			
		||||
                        transform.accept(box[x * size * size + y * size + z], new BlockPos(x, y, z));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void map(BiFunction<T, BlockPos, T> transform) {
 | 
			
		||||
            for (var x = 0; x < size; x++) {
 | 
			
		||||
                for (var y = 0; y < size; y++) {
 | 
			
		||||
                    for (var z = 0; z < size; z++) {
 | 
			
		||||
                        box[x * size * size + y * size + z] = transform.apply(box[x * size * size + y * size + z], new BlockPos(x, y, z));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static Set<WiredNodeImpl> nodes(WiredNetwork network) {
 | 
			
		||||
        return ((WiredNetworkImpl) network).nodes;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,91 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.command.arguments;
 | 
			
		||||
 | 
			
		||||
import com.mojang.brigadier.StringReader;
 | 
			
		||||
import com.mojang.brigadier.context.CommandContext;
 | 
			
		||||
import com.mojang.brigadier.context.StringRange;
 | 
			
		||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
 | 
			
		||||
import com.mojang.brigadier.suggestion.Suggestion;
 | 
			
		||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
 | 
			
		||||
import dan200.computercraft.shared.command.Exceptions;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import dan200.computercraft.test.core.ReplaceUnderscoresDisplayNameGenerator;
 | 
			
		||||
import org.junit.jupiter.api.DisplayNameGeneration;
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
import org.junit.jupiter.params.ParameterizedTest;
 | 
			
		||||
import org.junit.jupiter.params.provider.Arguments;
 | 
			
		||||
import org.junit.jupiter.params.provider.MethodSource;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.OptionalInt;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
 | 
			
		||||
import static org.hamcrest.MatcherAssert.assertThat;
 | 
			
		||||
import static org.hamcrest.Matchers.*;
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertEquals;
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertThrows;
 | 
			
		||||
 | 
			
		||||
@DisplayNameGeneration(ReplaceUnderscoresDisplayNameGenerator.class)
 | 
			
		||||
class ComputerSelectorTest {
 | 
			
		||||
    @ParameterizedTest(name = "{0}")
 | 
			
		||||
    @MethodSource("getArgumentTestCases")
 | 
			
		||||
    public void Parse_basic_inputs(String input, ComputerSelector expected) throws CommandSyntaxException {
 | 
			
		||||
        assertEquals(expected, ComputerSelector.parse(new StringReader(input)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static Arguments[] getArgumentTestCases() {
 | 
			
		||||
        return new Arguments[]{
 | 
			
		||||
            // Legacy selectors
 | 
			
		||||
            Arguments.of("@some_label", new ComputerSelector("@some_label", OptionalInt.empty(), null, OptionalInt.empty(), "some_label", null, null, null)),
 | 
			
		||||
            Arguments.of("~normal", new ComputerSelector("~normal", OptionalInt.empty(), null, OptionalInt.empty(), null, ComputerFamily.NORMAL, null, null)),
 | 
			
		||||
            Arguments.of("#123", new ComputerSelector("#123", OptionalInt.empty(), null, OptionalInt.of(123), null, null, null, null)),
 | 
			
		||||
            Arguments.of("123", new ComputerSelector("123", OptionalInt.of(123), null, OptionalInt.empty(), null, null, null, null)),
 | 
			
		||||
            // New selectors
 | 
			
		||||
            Arguments.of("@c[]", new ComputerSelector("@c[]", OptionalInt.empty(), null, OptionalInt.empty(), null, null, null, null)),
 | 
			
		||||
            Arguments.of("@c[instance=5e18f505-62f7-46f8-83f3-792f03224724]", new ComputerSelector("@c[instance=5e18f505-62f7-46f8-83f3-792f03224724]", OptionalInt.empty(), UUID.fromString("5e18f505-62f7-46f8-83f3-792f03224724"), OptionalInt.empty(), null, null, null, null)),
 | 
			
		||||
            Arguments.of("@c[id=123]", new ComputerSelector("@c[id=123]", OptionalInt.empty(), null, OptionalInt.of(123), null, null, null, null)),
 | 
			
		||||
            Arguments.of("@c[label=\"foo\"]", new ComputerSelector("@c[label=\"foo\"]", OptionalInt.empty(), null, OptionalInt.empty(), "foo", null, null, null)),
 | 
			
		||||
            Arguments.of("@c[family=normal]", new ComputerSelector("@c[family=normal]", OptionalInt.empty(), null, OptionalInt.empty(), null, ComputerFamily.NORMAL, null, null)),
 | 
			
		||||
            // Complex selectors
 | 
			
		||||
            Arguments.of("@c[ id = 123 , ]", new ComputerSelector("@c[ id = 123 , ]", OptionalInt.empty(), null, OptionalInt.of(123), null, null, null, null)),
 | 
			
		||||
            Arguments.of("@c[id=123,family=normal]", new ComputerSelector("@c[id=123,family=normal]", OptionalInt.empty(), null, OptionalInt.of(123), null, ComputerFamily.NORMAL, null, null)),
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void Fails_on_repeated_options() {
 | 
			
		||||
        var error = assertThrows(CommandSyntaxException.class, () -> ComputerSelector.parse(new StringReader("@c[id=1, id=2]")));
 | 
			
		||||
        assertEquals(Exceptions.ERROR_INAPPLICABLE_OPTION, error.getType());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void Complete_selector_components() {
 | 
			
		||||
        assertEquals(List.of(new Suggestion(StringRange.between(0, 1), "@c[")), suggest("@"));
 | 
			
		||||
        assertThat(suggest("@c["), hasItem(
 | 
			
		||||
            new Suggestion(StringRange.at(3), "family", ComputerSelector.options().get("family").tooltip())
 | 
			
		||||
        ));
 | 
			
		||||
        assertEquals(List.of(new Suggestion(StringRange.at(9), "=")), suggest("@c[family"));
 | 
			
		||||
        assertEquals(List.of(new Suggestion(StringRange.at(16), ",")), suggest("@c[family=normal"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void Complete_selector_family() {
 | 
			
		||||
        assertThat(suggest("@c[family="), containsInAnyOrder(
 | 
			
		||||
            new Suggestion(StringRange.at(10), "normal"),
 | 
			
		||||
            new Suggestion(StringRange.at(10), "advanced"),
 | 
			
		||||
            new Suggestion(StringRange.at(10), "command")
 | 
			
		||||
        ));
 | 
			
		||||
        assertThat(suggest("@c[family=n"), contains(
 | 
			
		||||
            new Suggestion(StringRange.between(10, 11), "normal")
 | 
			
		||||
        ));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private List<Suggestion> suggest(String input) {
 | 
			
		||||
        var context = new CommandContext<>(new Object(), "", Map.of(), null, null, List.of(), StringRange.at(0), null, null, false);
 | 
			
		||||
        return ComputerSelector.suggest(context, new SuggestionsBuilder(input, 0)).getNow(null).getList();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -23,7 +23,7 @@ class DfpwmStateTest {
 | 
			
		||||
 | 
			
		||||
        var state = new DfpwmState();
 | 
			
		||||
        state.pushBuffer(new ObjectLuaTable(inputTbl), input.length, Optional.empty());
 | 
			
		||||
        var result = state.pullPending(0);
 | 
			
		||||
        var result = state.pullPending(0).audio();
 | 
			
		||||
        var contents = new byte[result.remaining()];
 | 
			
		||||
        result.get(contents);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,42 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.peripheral.speaker;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.test.core.ArbitraryByteBuffer;
 | 
			
		||||
import io.netty.buffer.Unpooled;
 | 
			
		||||
import net.jqwik.api.*;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
 | 
			
		||||
import static org.hamcrest.MatcherAssert.assertThat;
 | 
			
		||||
import static org.hamcrest.Matchers.equalTo;
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertEquals;
 | 
			
		||||
 | 
			
		||||
class EncodedAudioTest {
 | 
			
		||||
    /**
 | 
			
		||||
     * Sends the audio on a roundtrip, ensuring that its contents are reassembled on the other end.
 | 
			
		||||
     *
 | 
			
		||||
     * @param audio The message to send.
 | 
			
		||||
     */
 | 
			
		||||
    @Property
 | 
			
		||||
    public void testRoundTrip(@ForAll("audio") EncodedAudio audio) {
 | 
			
		||||
        var buffer = new FriendlyByteBuf(Unpooled.directBuffer());
 | 
			
		||||
        audio.write(buffer);
 | 
			
		||||
 | 
			
		||||
        var converted = EncodedAudio.read(buffer);
 | 
			
		||||
        assertEquals(buffer.readableBytes(), 0, "Whole packet was read");
 | 
			
		||||
 | 
			
		||||
        assertThat("Messages are equal", converted, equalTo(converted));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Provide
 | 
			
		||||
    Arbitrary<EncodedAudio> audio() {
 | 
			
		||||
        return Combinators.combine(
 | 
			
		||||
            Arbitraries.integers(),
 | 
			
		||||
            Arbitraries.integers(),
 | 
			
		||||
            Arbitraries.of(true, false),
 | 
			
		||||
            ArbitraryByteBuffer.bytes().ofMaxSize(1000)
 | 
			
		||||
        ).as(EncodedAudio::new);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -11,11 +11,15 @@ import dan200.computercraft.core.apis.TermAPI
 | 
			
		||||
import dan200.computercraft.core.computer.ComputerSide
 | 
			
		||||
import dan200.computercraft.gametest.api.*
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry
 | 
			
		||||
import dan200.computercraft.test.core.assertArrayEquals
 | 
			
		||||
import dan200.computercraft.test.core.computer.getApi
 | 
			
		||||
import net.minecraft.core.BlockPos
 | 
			
		||||
import net.minecraft.core.Direction
 | 
			
		||||
import net.minecraft.gametest.framework.GameTest
 | 
			
		||||
import net.minecraft.gametest.framework.GameTestHelper
 | 
			
		||||
import net.minecraft.world.InteractionHand
 | 
			
		||||
import net.minecraft.world.item.ItemStack
 | 
			
		||||
import net.minecraft.world.item.Items
 | 
			
		||||
import net.minecraft.world.level.block.Blocks
 | 
			
		||||
import net.minecraft.world.level.block.LeverBlock
 | 
			
		||||
import net.minecraft.world.level.block.RedstoneLampBlock
 | 
			
		||||
@@ -101,6 +105,17 @@ class Computer_Test {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check chest peripherals are reattached with a new size.
 | 
			
		||||
     */
 | 
			
		||||
    @GameTest
 | 
			
		||||
    fun Chest_resizes_on_change(context: GameTestHelper) = context.sequence {
 | 
			
		||||
        thenOnComputer { callPeripheral("right", "size").assertArrayEquals(27) }
 | 
			
		||||
        thenExecute { context.placeItemAt(ItemStack(Items.CHEST), BlockPos(2, 2, 2), Direction.WEST) }
 | 
			
		||||
        thenIdle(1)
 | 
			
		||||
        thenOnComputer { callPeripheral("right", "size").assertArrayEquals(54) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check the client can open the computer UI and interact with it.
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -7,18 +7,21 @@ package dan200.computercraft.gametest
 | 
			
		||||
import dan200.computercraft.api.lua.ObjectArguments
 | 
			
		||||
import dan200.computercraft.core.apis.PeripheralAPI
 | 
			
		||||
import dan200.computercraft.core.computer.ComputerSide
 | 
			
		||||
import dan200.computercraft.gametest.api.getBlockEntity
 | 
			
		||||
import dan200.computercraft.gametest.api.sequence
 | 
			
		||||
import dan200.computercraft.gametest.api.thenOnComputer
 | 
			
		||||
import dan200.computercraft.gametest.api.thenStartComputer
 | 
			
		||||
import dan200.computercraft.gametest.api.*
 | 
			
		||||
import dan200.computercraft.impl.network.wired.WiredNodeImpl
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry
 | 
			
		||||
import dan200.computercraft.shared.peripheral.modem.wired.CableBlock
 | 
			
		||||
import dan200.computercraft.shared.peripheral.modem.wired.CableModemVariant
 | 
			
		||||
import dan200.computercraft.test.core.assertArrayEquals
 | 
			
		||||
import dan200.computercraft.test.core.computer.LuaTaskContext
 | 
			
		||||
import dan200.computercraft.test.core.computer.getApi
 | 
			
		||||
import net.minecraft.core.BlockPos
 | 
			
		||||
import net.minecraft.core.Direction
 | 
			
		||||
import net.minecraft.gametest.framework.GameTest
 | 
			
		||||
import net.minecraft.gametest.framework.GameTestHelper
 | 
			
		||||
import net.minecraft.world.item.ItemStack
 | 
			
		||||
import net.minecraft.world.item.Items
 | 
			
		||||
import net.minecraft.world.level.block.Blocks
 | 
			
		||||
import org.junit.jupiter.api.Assertions.assertEquals
 | 
			
		||||
import kotlin.time.Duration.Companion.milliseconds
 | 
			
		||||
 | 
			
		||||
@@ -83,7 +86,86 @@ class Modem_Test {
 | 
			
		||||
        thenExecute {
 | 
			
		||||
            val modem1 = helper.getBlockEntity(BlockPos(1, 2, 1), ModRegistry.BlockEntities.WIRED_MODEM_FULL.get())
 | 
			
		||||
            val modem2 = helper.getBlockEntity(BlockPos(3, 2, 1), ModRegistry.BlockEntities.WIRED_MODEM_FULL.get())
 | 
			
		||||
            assertEquals(modem1.element.node.network, modem2.element.node.network, "On the same network")
 | 
			
		||||
            assertEquals((modem1.element.node as WiredNodeImpl).network, (modem2.element.node as WiredNodeImpl).network, "On the same network")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Modems do not include the current peripheral when attached.
 | 
			
		||||
     */
 | 
			
		||||
    @GameTest
 | 
			
		||||
    fun Cable_modem_does_not_report_self(helper: GameTestHelper) = helper.sequence {
 | 
			
		||||
        // Modem does not report the computer as a peripheral.
 | 
			
		||||
        thenOnComputer { assertEquals(listOf("back", "right"), getPeripheralNames()) }
 | 
			
		||||
 | 
			
		||||
        // However, if we connect the network, the other modem does.
 | 
			
		||||
        thenExecute {
 | 
			
		||||
            helper.setBlock(
 | 
			
		||||
                BlockPos(1, 2, 3),
 | 
			
		||||
                ModRegistry.Blocks.CABLE.get().defaultBlockState().setValue(CableBlock.CABLE, true),
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
        thenIdle(2)
 | 
			
		||||
        thenOnComputer { assertEquals(listOf("back", "computer_0", "right"), getPeripheralNames()) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Modems do not include the current peripheral when attached.
 | 
			
		||||
     */
 | 
			
		||||
    @GameTest
 | 
			
		||||
    fun Full_block_modem_does_not_report_self(helper: GameTestHelper) = helper.sequence {
 | 
			
		||||
        // Modem does not report the computer as a peripheral.
 | 
			
		||||
        thenOnComputer { assertEquals(listOf("back", "right"), getPeripheralNames()) }
 | 
			
		||||
 | 
			
		||||
        // However, if we connect the network, the other modem does.
 | 
			
		||||
        thenExecute {
 | 
			
		||||
            helper.setBlock(
 | 
			
		||||
                BlockPos(1, 2, 3),
 | 
			
		||||
                ModRegistry.Blocks.CABLE.get().defaultBlockState().setValue(CableBlock.CABLE, true),
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
        thenIdle(2)
 | 
			
		||||
        thenOnComputer { assertEquals(listOf("back", "computer_1", "right"), getPeripheralNames()) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Test wired modems (without a cable) drop an item when the adjacent block is removed.
 | 
			
		||||
     */
 | 
			
		||||
    @GameTest
 | 
			
		||||
    fun Modem_drops_when_neighbour_removed(helper: GameTestHelper) = helper.sequence {
 | 
			
		||||
        thenExecute {
 | 
			
		||||
            helper.setBlock(BlockPos(2, 3, 2), Blocks.AIR)
 | 
			
		||||
            helper.assertItemEntityPresent(ModRegistry.Items.WIRED_MODEM.get(), BlockPos(2, 2, 2), 0.0)
 | 
			
		||||
            helper.assertBlockPresent(Blocks.AIR, BlockPos(2, 2, 2))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Test wired modems (with a cable) drop an item, but keep their cable when the adjacent block is removed.
 | 
			
		||||
     */
 | 
			
		||||
    @GameTest
 | 
			
		||||
    fun Modem_keeps_cable_when_neighbour_removed(helper: GameTestHelper) = helper.sequence {
 | 
			
		||||
        thenExecute {
 | 
			
		||||
            helper.setBlock(BlockPos(2, 3, 2), Blocks.AIR)
 | 
			
		||||
            helper.assertItemEntityPresent(ModRegistry.Items.WIRED_MODEM.get(), BlockPos(2, 2, 2), 0.0)
 | 
			
		||||
            helper.assertBlockIs(BlockPos(2, 2, 2)) {
 | 
			
		||||
                it.block == ModRegistry.Blocks.CABLE.get() && it.getValue(CableBlock.MODEM) == CableModemVariant.None && it.getValue(CableBlock.CABLE)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check chest peripherals are reattached with a new size.
 | 
			
		||||
     */
 | 
			
		||||
    @GameTest
 | 
			
		||||
    fun Chest_resizes_on_change(context: GameTestHelper) = context.sequence {
 | 
			
		||||
        thenOnComputer {
 | 
			
		||||
            callRemotePeripheral("minecraft:chest_0", "size").assertArrayEquals(27)
 | 
			
		||||
        }
 | 
			
		||||
        thenExecute { context.placeItemAt(ItemStack(Items.CHEST), BlockPos(2, 2, 2), Direction.WEST) }
 | 
			
		||||
        thenIdle(1)
 | 
			
		||||
        thenOnComputer {
 | 
			
		||||
            callRemotePeripheral("minecraft:chest_0", "size").assertArrayEquals(54)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -105,7 +187,7 @@ private suspend fun LuaTaskContext.getPeripheralNames(): List<String> {
 | 
			
		||||
        if (!peripheral.isPresent(side)) continue
 | 
			
		||||
        peripherals.add(side)
 | 
			
		||||
 | 
			
		||||
        val hasType = peripheral.hasType(side, "modem")
 | 
			
		||||
        val hasType = peripheral.hasType(side, "peripheral_hub")
 | 
			
		||||
        if (hasType == null || hasType[0] != true) continue
 | 
			
		||||
 | 
			
		||||
        val names = peripheral.call(context, ObjectArguments(side, "getNamesRemote")).await() ?: continue
 | 
			
		||||
@@ -116,3 +198,22 @@ private suspend fun LuaTaskContext.getPeripheralNames(): List<String> {
 | 
			
		||||
    peripherals.sort()
 | 
			
		||||
    return peripherals
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private suspend fun LuaTaskContext.callRemotePeripheral(name: String, method: String, vararg args: Any): Array<out Any?>? {
 | 
			
		||||
    val peripheral = getApi<PeripheralAPI>()
 | 
			
		||||
    if (peripheral.isPresent(name)) return peripheral.call(context, ObjectArguments(name, method, *args)).await()
 | 
			
		||||
 | 
			
		||||
    for (side in ComputerSide.NAMES) {
 | 
			
		||||
        if (!peripheral.isPresent(side)) continue
 | 
			
		||||
 | 
			
		||||
        val hasType = peripheral.hasType(side, "peripheral_hub")
 | 
			
		||||
        if (hasType == null || hasType[0] != true) continue
 | 
			
		||||
 | 
			
		||||
        val isPresent = peripheral.call(context, ObjectArguments(side, "isPresentRemote", name)).await() ?: continue
 | 
			
		||||
        if (isPresent[0] as Boolean) {
 | 
			
		||||
            return peripheral.call(context, ObjectArguments(side, "callRemote", name, method, *args)).await()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    throw IllegalArgumentException("No such peripheral $name")
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ class Pocket_Computer_Test {
 | 
			
		||||
        // And ensure its synced to the client.
 | 
			
		||||
        thenIdle(4)
 | 
			
		||||
        thenOnClient {
 | 
			
		||||
            val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)
 | 
			
		||||
            val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)!!
 | 
			
		||||
            assertEquals(ComputerState.ON, pocketComputer.state)
 | 
			
		||||
 | 
			
		||||
            val term = pocketComputer.terminal
 | 
			
		||||
@@ -54,7 +54,7 @@ class Pocket_Computer_Test {
 | 
			
		||||
        // And ensure the new computer state and terminal are sent.
 | 
			
		||||
        thenIdle(4)
 | 
			
		||||
        thenOnClient {
 | 
			
		||||
            val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)
 | 
			
		||||
            val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)!!
 | 
			
		||||
            assertEquals(ComputerState.BLINKING, pocketComputer.state)
 | 
			
		||||
 | 
			
		||||
            val term = pocketComputer.terminal
 | 
			
		||||
 
 | 
			
		||||
@@ -329,8 +329,6 @@ class Turtle_Test {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks turtles can be cleaned in cauldrons.
 | 
			
		||||
     *
 | 
			
		||||
     * Currently not required as turtles can no longer right-click cauldrons.
 | 
			
		||||
     */
 | 
			
		||||
    @GameTest
 | 
			
		||||
    fun Cleaned_with_cauldrons(helper: GameTestHelper) = helper.sequence {
 | 
			
		||||
@@ -643,7 +641,20 @@ class Turtle_Test {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO: Turtle sucking from items
 | 
			
		||||
    /**
 | 
			
		||||
     * `turtle.suck` only pulls for the current side.
 | 
			
		||||
     */
 | 
			
		||||
    @GameTest
 | 
			
		||||
    fun Sided_suck(helper: GameTestHelper) = helper.sequence {
 | 
			
		||||
        thenOnComputer {
 | 
			
		||||
            turtle.suckUp(Optional.empty()).await().assertArrayEquals(true)
 | 
			
		||||
            turtle.getItemDetail(context, Optional.empty(), Optional.empty()).await().assertArrayEquals(
 | 
			
		||||
                mapOf("name" to "minecraft:iron_ingot", "count" to 8),
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            turtle.suckUp(Optional.empty()).await().assertArrayEquals(false, "No items to take")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Render turtles as an item.
 | 
			
		||||
 
 | 
			
		||||
@@ -20,15 +20,19 @@ import net.minecraft.core.registries.BuiltInRegistries
 | 
			
		||||
import net.minecraft.gametest.framework.*
 | 
			
		||||
import net.minecraft.resources.ResourceLocation
 | 
			
		||||
import net.minecraft.world.Container
 | 
			
		||||
import net.minecraft.world.InteractionHand
 | 
			
		||||
import net.minecraft.world.entity.Entity
 | 
			
		||||
import net.minecraft.world.entity.EntityType
 | 
			
		||||
import net.minecraft.world.item.ItemStack
 | 
			
		||||
import net.minecraft.world.item.context.UseOnContext
 | 
			
		||||
import net.minecraft.world.level.block.Blocks
 | 
			
		||||
import net.minecraft.world.level.block.entity.BarrelBlockEntity
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntity
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntityType
 | 
			
		||||
import net.minecraft.world.level.block.state.BlockState
 | 
			
		||||
import net.minecraft.world.level.block.state.properties.Property
 | 
			
		||||
import net.minecraft.world.phys.BlockHitResult
 | 
			
		||||
import net.minecraft.world.phys.Vec3
 | 
			
		||||
import org.hamcrest.Matchers
 | 
			
		||||
import org.hamcrest.StringDescription
 | 
			
		||||
 | 
			
		||||
@@ -306,3 +310,16 @@ fun GameTestHelper.setContainerItem(pos: BlockPos, slot: Int, item: ItemStack) {
 | 
			
		||||
    container.setItem(slot, item)
 | 
			
		||||
    container.setChanged()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An alternative version ot [GameTestHelper.placeAt], which sets the player's held item first.
 | 
			
		||||
 *
 | 
			
		||||
 * This is required for compatibility with Forge, which uses the in-hand stack, rather than the stack requested.
 | 
			
		||||
 */
 | 
			
		||||
fun GameTestHelper.placeItemAt(stack: ItemStack, pos: BlockPos, direction: Direction) {
 | 
			
		||||
    val player = makeMockPlayer()
 | 
			
		||||
    player.setItemInHand(InteractionHand.MAIN_HAND, stack)
 | 
			
		||||
    val absolutePos = absolutePos(pos.relative(direction))
 | 
			
		||||
    val hit = BlockHitResult(Vec3.atCenterOf(absolutePos), direction, absolutePos, false)
 | 
			
		||||
    stack.useOn(UseOnContext(player, InteractionHand.MAIN_HAND, hit))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										138
									
								
								projects/common/src/testMod/resources/data/cctest/structures/computer_test.chest_resizes_on_change.snbt
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								projects/common/src/testMod/resources/data/cctest/structures/computer_test.chest_resizes_on_change.snbt
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,138 @@
 | 
			
		||||
{
 | 
			
		||||
    DataVersion: 3465,
 | 
			
		||||
    size: [5, 5, 5],
 | 
			
		||||
    data: [
 | 
			
		||||
        {pos: [0, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 2], state: "minecraft:chest{facing:north,type:single,waterlogged:false}", nbt: {Items: [], id: "minecraft:chest"}},
 | 
			
		||||
        {pos: [2, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 2], state: "computercraft:computer_normal{facing:north,state:on}", nbt: {ComputerId: 1, Label: "computer_test.chest_resizes_on_change", On: 1b, id: "computercraft:computer_normal"}},
 | 
			
		||||
        {pos: [3, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 4], state: "minecraft:air"}
 | 
			
		||||
    ],
 | 
			
		||||
    entities: [],
 | 
			
		||||
    palette: [
 | 
			
		||||
        "minecraft:polished_andesite",
 | 
			
		||||
        "minecraft:air",
 | 
			
		||||
        "minecraft:chest{facing:north,type:single,waterlogged:false}",
 | 
			
		||||
        "computercraft:computer_normal{facing:north,state:on}"
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,139 @@
 | 
			
		||||
{
 | 
			
		||||
    DataVersion: 3465,
 | 
			
		||||
    size: [5, 5, 5],
 | 
			
		||||
    data: [
 | 
			
		||||
        {pos: [0, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 2], state: "computercraft:cable{cable:true,down:false,east:true,modem:east_off_peripheral,north:false,south:false,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 1b, PeripheralId: 0, PeripheralType: "computer", id: "computercraft:cable"}},
 | 
			
		||||
        {pos: [1, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 2], state: "computercraft:computer_normal{facing:north,state:on}", nbt: {ComputerId: 1, Label: "modem_test.cable_modem_does_not_report_self", On: 1b, id: "computercraft:computer_normal"}},
 | 
			
		||||
        {pos: [2, 1, 3], state: "computercraft:cable{cable:true,down:false,east:false,modem:north_off,north:true,south:false,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
 | 
			
		||||
        {pos: [2, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 4], state: "minecraft:air"}
 | 
			
		||||
    ],
 | 
			
		||||
    entities: [],
 | 
			
		||||
    palette: [
 | 
			
		||||
        "minecraft:polished_andesite",
 | 
			
		||||
        "minecraft:air",
 | 
			
		||||
        "computercraft:cable{cable:true,down:false,east:true,modem:east_off_peripheral,north:false,south:false,up:false,waterlogged:false,west:false}",
 | 
			
		||||
        "computercraft:computer_normal{facing:north,state:on}",
 | 
			
		||||
        "computercraft:cable{cable:true,down:false,east:false,modem:north_off,north:true,south:false,up:false,waterlogged:false,west:false}"
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										139
									
								
								projects/common/src/testMod/resources/data/cctest/structures/modem_test.chest_resizes_on_change.snbt
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								projects/common/src/testMod/resources/data/cctest/structures/modem_test.chest_resizes_on_change.snbt
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,139 @@
 | 
			
		||||
{
 | 
			
		||||
    DataVersion: 3465,
 | 
			
		||||
    size: [5, 5, 5],
 | 
			
		||||
    data: [
 | 
			
		||||
        {pos: [0, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 2], state: "minecraft:chest{facing:north,type:single,waterlogged:false}", nbt: {Items: [], id: "minecraft:chest"}},
 | 
			
		||||
        {pos: [2, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 1], state: "computercraft:computer_normal{facing:north,state:on}", nbt: {ComputerId: 1, Label: "modem_test.chest_resizes_on_change", On: 1b, id: "computercraft:computer_normal"}},
 | 
			
		||||
        {pos: [3, 1, 2], state: "computercraft:wired_modem_full{modem:false,peripheral:true}", nbt: {PeripheralId2: 2, PeripheralId4: 0, PeripheralType2: "computer", PeripheralType4: "minecraft:chest", id: "computercraft:wired_modem_full"}},
 | 
			
		||||
        {pos: [3, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 4], state: "minecraft:air"}
 | 
			
		||||
    ],
 | 
			
		||||
    entities: [],
 | 
			
		||||
    palette: [
 | 
			
		||||
        "minecraft:polished_andesite",
 | 
			
		||||
        "minecraft:air",
 | 
			
		||||
        "minecraft:chest{facing:north,type:single,waterlogged:false}",
 | 
			
		||||
        "computercraft:computer_normal{facing:north,state:on}",
 | 
			
		||||
        "computercraft:wired_modem_full{modem:false,peripheral:true}"
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,139 @@
 | 
			
		||||
{
 | 
			
		||||
    DataVersion: 3465,
 | 
			
		||||
    size: [5, 5, 5],
 | 
			
		||||
    data: [
 | 
			
		||||
        {pos: [0, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 2], state: "computercraft:wired_modem_full{modem:false,peripheral:true}", nbt: {PeripheralAccess: 1b, PeripheralId5: 1, PeripheralType5: "computer", id: "computercraft:wired_modem_full"}},
 | 
			
		||||
        {pos: [1, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 2], state: "computercraft:computer_normal{facing:north,state:on}", nbt: {ComputerId: 1, Label: "modem_test.full_block_modem_does_not_report_self", On: 1b, id: "computercraft:computer_normal"}},
 | 
			
		||||
        {pos: [2, 1, 3], state: "computercraft:wired_modem_full{modem:false,peripheral:false}", nbt: {PeripheralAccess: 0b, id: "computercraft:wired_modem_full"}},
 | 
			
		||||
        {pos: [2, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 4], state: "minecraft:air"}
 | 
			
		||||
    ],
 | 
			
		||||
    entities: [],
 | 
			
		||||
    palette: [
 | 
			
		||||
        "minecraft:polished_andesite",
 | 
			
		||||
        "minecraft:air",
 | 
			
		||||
        "computercraft:wired_modem_full{modem:false,peripheral:true}",
 | 
			
		||||
        "computercraft:computer_normal{facing:north,state:on}",
 | 
			
		||||
        "computercraft:wired_modem_full{modem:false,peripheral:false}"
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
@@ -28,7 +28,7 @@
 | 
			
		||||
        {pos: [4, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 1, 0], state: "computercraft:printer{bottom:false,facing:north,top:false}", nbt: {Items: [], PageTitle: "", Printing: 0b, id: "computercraft:printer", term_bgColour: 15, term_cursorBlink: 0b, term_cursorX: 0, term_cursorY: 0, term_palette: [I; 1118481, 13388876, 5744206, 8349260, 3368652, 11691749, 5020082, 10066329, 5000268, 15905484, 8375321, 14605932, 10072818, 15040472, 15905331, 15790320], term_textBgColour_0: "fffffffffffffffffffffffff", term_textBgColour_1: "fffffffffffffffffffffffff", term_textBgColour_10: "fffffffffffffffffffffffff", term_textBgColour_11: "fffffffffffffffffffffffff", term_textBgColour_12: "fffffffffffffffffffffffff", term_textBgColour_13: "fffffffffffffffffffffffff", term_textBgColour_14: "fffffffffffffffffffffffff", term_textBgColour_15: "fffffffffffffffffffffffff", term_textBgColour_16: "fffffffffffffffffffffffff", term_textBgColour_17: "fffffffffffffffffffffffff", term_textBgColour_18: "fffffffffffffffffffffffff", term_textBgColour_19: "fffffffffffffffffffffffff", term_textBgColour_2: "fffffffffffffffffffffffff", term_textBgColour_20: "fffffffffffffffffffffffff", term_textBgColour_3: "fffffffffffffffffffffffff", term_textBgColour_4: "fffffffffffffffffffffffff", term_textBgColour_5: "fffffffffffffffffffffffff", term_textBgColour_6: "fffffffffffffffffffffffff", term_textBgColour_7: "fffffffffffffffffffffffff", term_textBgColour_8: "fffffffffffffffffffffffff", term_textBgColour_9: "fffffffffffffffffffffffff", term_textColour: 0, term_textColour_0: "0000000000000000000000000", term_textColour_1: "0000000000000000000000000", term_textColour_10: "0000000000000000000000000", term_textColour_11: "0000000000000000000000000", term_textColour_12: "0000000000000000000000000", term_textColour_13: "0000000000000000000000000", term_textColour_14: "0000000000000000000000000", term_textColour_15: "0000000000000000000000000", term_textColour_16: "0000000000000000000000000", term_textColour_17: "0000000000000000000000000", term_textColour_18: "0000000000000000000000000", term_textColour_19: "0000000000000000000000000", term_textColour_2: "0000000000000000000000000", term_textColour_20: "0000000000000000000000000", term_textColour_3: "0000000000000000000000000", term_textColour_4: "0000000000000000000000000", term_textColour_5: "0000000000000000000000000", term_textColour_6: "0000000000000000000000000", term_textColour_7: "0000000000000000000000000", term_textColour_8: "0000000000000000000000000", term_textColour_9: "0000000000000000000000000", term_text_0: "                         ", term_text_1: "                         ", term_text_10: "                         ", term_text_11: "                         ", term_text_12: "                         ", term_text_13: "                         ", term_text_14: "                         ", term_text_15: "                         ", term_text_16: "                         ", term_text_17: "                         ", term_text_18: "                         ", term_text_19: "                         ", term_text_2: "                         ", term_text_20: "                         ", term_text_3: "                         ", term_text_4: "                         ", term_text_5: "                         ", term_text_6: "                         ", term_text_7: "                         ", term_text_8: "                         ", term_text_9: "                         "}},
 | 
			
		||||
        {pos: [0, 1, 1], state: "computercraft:cable{cable:true,down:false,east:true,modem:north_on,north:true,south:false,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 1b, PeripheralId: 1, PeripheralType: "printer", id: "computercraft:cable"}},
 | 
			
		||||
        {pos: [0, 1, 1], state: "computercraft:cable{cable:true,down:false,east:true,modem:north_off_peripheral,north:true,south:false,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 1b, PeripheralId: 1, PeripheralType: "printer", id: "computercraft:cable"}},
 | 
			
		||||
        {pos: [0, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 4], state: "computercraft:monitor_advanced{facing:north,orientation:north,state:none}", nbt: {Height: 1, Width: 1, XIndex: 0, YIndex: 0, id: "computercraft:monitor_advanced"}},
 | 
			
		||||
@@ -36,7 +36,7 @@
 | 
			
		||||
        {pos: [1, 1, 1], state: "computercraft:cable{cable:true,down:false,east:false,modem:none,north:false,south:true,up:false,waterlogged:false,west:true}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
 | 
			
		||||
        {pos: [1, 1, 2], state: "computercraft:cable{cable:true,down:false,east:false,modem:none,north:true,south:true,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
 | 
			
		||||
        {pos: [1, 1, 3], state: "computercraft:cable{cable:true,down:false,east:false,modem:none,north:true,south:true,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
 | 
			
		||||
        {pos: [1, 1, 4], state: "computercraft:cable{cable:true,down:false,east:false,modem:west_on,north:true,south:false,up:false,waterlogged:false,west:true}", nbt: {PeripheralAccess: 1b, PeripheralId: 1, PeripheralType: "monitor", id: "computercraft:cable"}},
 | 
			
		||||
        {pos: [1, 1, 4], state: "computercraft:cable{cable:true,down:false,east:false,modem:west_off_peripheral,north:true,south:false,up:false,waterlogged:false,west:true}", nbt: {PeripheralAccess: 1b, PeripheralId: 1, PeripheralType: "monitor", id: "computercraft:cable"}},
 | 
			
		||||
        {pos: [2, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 2], state: "minecraft:air"},
 | 
			
		||||
@@ -133,11 +133,11 @@
 | 
			
		||||
        "minecraft:polished_andesite",
 | 
			
		||||
        "minecraft:air",
 | 
			
		||||
        "computercraft:printer{bottom:false,facing:north,top:false}",
 | 
			
		||||
        "computercraft:cable{cable:true,down:false,east:true,modem:north_on,north:true,south:false,up:false,waterlogged:false,west:false}",
 | 
			
		||||
        "computercraft:cable{cable:true,down:false,east:true,modem:north_off_peripheral,north:true,south:false,up:false,waterlogged:false,west:false}",
 | 
			
		||||
        "computercraft:monitor_advanced{facing:north,orientation:north,state:none}",
 | 
			
		||||
        "computercraft:cable{cable:true,down:false,east:false,modem:none,north:false,south:true,up:false,waterlogged:false,west:true}",
 | 
			
		||||
        "computercraft:cable{cable:true,down:false,east:false,modem:none,north:true,south:true,up:false,waterlogged:false,west:false}",
 | 
			
		||||
        "computercraft:cable{cable:true,down:false,east:false,modem:west_on,north:true,south:false,up:false,waterlogged:false,west:true}",
 | 
			
		||||
        "computercraft:cable{cable:true,down:false,east:false,modem:west_off_peripheral,north:true,south:false,up:false,waterlogged:false,west:true}",
 | 
			
		||||
        "computercraft:cable{cable:true,down:false,east:true,modem:none,north:false,south:false,up:false,waterlogged:false,west:false}",
 | 
			
		||||
        "computercraft:computer_advanced{facing:north,state:blinking}",
 | 
			
		||||
        "computercraft:cable{cable:true,down:false,east:false,modem:north_off,north:true,south:false,up:false,waterlogged:false,west:true}"
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@
 | 
			
		||||
        {pos: [4, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 1, 0], state: "computercraft:printer{bottom:false,facing:north,top:false}", nbt: {Items: [], PageTitle: "", Printing: 0b, id: "computercraft:printer", term_bgColour: 15, term_cursorBlink: 0b, term_cursorX: 0, term_cursorY: 0, term_palette: [I; 1118481, 13388876, 5744206, 8349260, 3368652, 11691749, 5020082, 10066329, 5000268, 15905484, 8375321, 14605932, 10072818, 15040472, 15905331, 15790320], term_textBgColour_0: "fffffffffffffffffffffffff", term_textBgColour_1: "fffffffffffffffffffffffff", term_textBgColour_10: "fffffffffffffffffffffffff", term_textBgColour_11: "fffffffffffffffffffffffff", term_textBgColour_12: "fffffffffffffffffffffffff", term_textBgColour_13: "fffffffffffffffffffffffff", term_textBgColour_14: "fffffffffffffffffffffffff", term_textBgColour_15: "fffffffffffffffffffffffff", term_textBgColour_16: "fffffffffffffffffffffffff", term_textBgColour_17: "fffffffffffffffffffffffff", term_textBgColour_18: "fffffffffffffffffffffffff", term_textBgColour_19: "fffffffffffffffffffffffff", term_textBgColour_2: "fffffffffffffffffffffffff", term_textBgColour_20: "fffffffffffffffffffffffff", term_textBgColour_3: "fffffffffffffffffffffffff", term_textBgColour_4: "fffffffffffffffffffffffff", term_textBgColour_5: "fffffffffffffffffffffffff", term_textBgColour_6: "fffffffffffffffffffffffff", term_textBgColour_7: "fffffffffffffffffffffffff", term_textBgColour_8: "fffffffffffffffffffffffff", term_textBgColour_9: "fffffffffffffffffffffffff", term_textColour: 0, term_textColour_0: "0000000000000000000000000", term_textColour_1: "0000000000000000000000000", term_textColour_10: "0000000000000000000000000", term_textColour_11: "0000000000000000000000000", term_textColour_12: "0000000000000000000000000", term_textColour_13: "0000000000000000000000000", term_textColour_14: "0000000000000000000000000", term_textColour_15: "0000000000000000000000000", term_textColour_16: "0000000000000000000000000", term_textColour_17: "0000000000000000000000000", term_textColour_18: "0000000000000000000000000", term_textColour_19: "0000000000000000000000000", term_textColour_2: "0000000000000000000000000", term_textColour_20: "0000000000000000000000000", term_textColour_3: "0000000000000000000000000", term_textColour_4: "0000000000000000000000000", term_textColour_5: "0000000000000000000000000", term_textColour_6: "0000000000000000000000000", term_textColour_7: "0000000000000000000000000", term_textColour_8: "0000000000000000000000000", term_textColour_9: "0000000000000000000000000", term_text_0: "                         ", term_text_1: "                         ", term_text_10: "                         ", term_text_11: "                         ", term_text_12: "                         ", term_text_13: "                         ", term_text_14: "                         ", term_text_15: "                         ", term_text_16: "                         ", term_text_17: "                         ", term_text_18: "                         ", term_text_19: "                         ", term_text_2: "                         ", term_text_20: "                         ", term_text_3: "                         ", term_text_4: "                         ", term_text_5: "                         ", term_text_6: "                         ", term_text_7: "                         ", term_text_8: "                         ", term_text_9: "                         "}},
 | 
			
		||||
        {pos: [0, 1, 1], state: "computercraft:cable{cable:true,down:false,east:false,modem:north_on,north:true,south:true,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 1b, PeripheralId: 0, PeripheralType: "printer", id: "computercraft:cable"}},
 | 
			
		||||
        {pos: [0, 1, 1], state: "computercraft:cable{cable:true,down:false,east:false,modem:north_off_peripheral,north:true,south:true,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 1b, PeripheralId: 0, PeripheralType: "printer", id: "computercraft:cable"}},
 | 
			
		||||
        {pos: [0, 1, 2], state: "computercraft:cable{cable:true,down:false,east:true,modem:none,north:true,south:false,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
 | 
			
		||||
        {pos: [0, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 4], state: "computercraft:monitor_advanced{facing:north,orientation:north,state:none}", nbt: {Height: 1, Width: 1, XIndex: 0, YIndex: 0, id: "computercraft:monitor_advanced"}},
 | 
			
		||||
@@ -36,7 +36,7 @@
 | 
			
		||||
        {pos: [1, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 2], state: "computercraft:cable{cable:true,down:false,east:true,modem:none,north:false,south:true,up:false,waterlogged:false,west:true}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
 | 
			
		||||
        {pos: [1, 1, 3], state: "computercraft:cable{cable:true,down:false,east:false,modem:none,north:true,south:true,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
 | 
			
		||||
        {pos: [1, 1, 4], state: "computercraft:cable{cable:true,down:false,east:false,modem:west_on,north:true,south:false,up:false,waterlogged:false,west:true}", nbt: {PeripheralAccess: 1b, PeripheralId: 0, PeripheralType: "monitor", id: "computercraft:cable"}},
 | 
			
		||||
        {pos: [1, 1, 4], state: "computercraft:cable{cable:true,down:false,east:false,modem:west_off_peripheral,north:true,south:false,up:false,waterlogged:false,west:true}", nbt: {PeripheralAccess: 1b, PeripheralId: 0, PeripheralType: "monitor", id: "computercraft:cable"}},
 | 
			
		||||
        {pos: [2, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 2], state: "computercraft:cable{cable:true,down:false,east:true,modem:none,north:false,south:false,up:false,waterlogged:false,west:true}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
 | 
			
		||||
@@ -133,12 +133,12 @@
 | 
			
		||||
        "minecraft:polished_andesite",
 | 
			
		||||
        "minecraft:air",
 | 
			
		||||
        "computercraft:printer{bottom:false,facing:north,top:false}",
 | 
			
		||||
        "computercraft:cable{cable:true,down:false,east:false,modem:north_on,north:true,south:true,up:false,waterlogged:false,west:false}",
 | 
			
		||||
        "computercraft:cable{cable:true,down:false,east:false,modem:north_off_peripheral,north:true,south:true,up:false,waterlogged:false,west:false}",
 | 
			
		||||
        "computercraft:cable{cable:true,down:false,east:true,modem:none,north:true,south:false,up:false,waterlogged:false,west:false}",
 | 
			
		||||
        "computercraft:monitor_advanced{facing:north,orientation:north,state:none}",
 | 
			
		||||
        "computercraft:cable{cable:true,down:false,east:true,modem:none,north:false,south:true,up:false,waterlogged:false,west:true}",
 | 
			
		||||
        "computercraft:cable{cable:true,down:false,east:false,modem:none,north:true,south:true,up:false,waterlogged:false,west:false}",
 | 
			
		||||
        "computercraft:cable{cable:true,down:false,east:false,modem:west_on,north:true,south:false,up:false,waterlogged:false,west:true}",
 | 
			
		||||
        "computercraft:cable{cable:true,down:false,east:false,modem:west_off_peripheral,north:true,south:false,up:false,waterlogged:false,west:true}",
 | 
			
		||||
        "computercraft:cable{cable:true,down:false,east:true,modem:none,north:false,south:false,up:false,waterlogged:false,west:true}",
 | 
			
		||||
        "computercraft:cable{cable:true,down:false,east:true,modem:east_off,north:false,south:false,up:false,waterlogged:false,west:true}",
 | 
			
		||||
        "computercraft:computer_advanced{facing:north,state:blinking}"
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,138 @@
 | 
			
		||||
{
 | 
			
		||||
    DataVersion: 3465,
 | 
			
		||||
    size: [5, 5, 5],
 | 
			
		||||
    data: [
 | 
			
		||||
        {pos: [0, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 1], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [1, 1, 2], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [1, 1, 3], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [1, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 1], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [2, 1, 2], state: "computercraft:cable{cable:false,down:false,east:false,modem:up_off,north:false,south:false,up:false,waterlogged:false,west:false}", nbt: {id: "computercraft:cable"}},
 | 
			
		||||
        {pos: [2, 1, 3], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [2, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 1], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [3, 1, 2], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [3, 1, 3], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [3, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 1], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [1, 2, 2], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [1, 2, 3], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [1, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 1], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [2, 2, 2], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [2, 2, 3], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [2, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 1], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [3, 2, 2], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [3, 2, 3], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [3, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 4], state: "minecraft:air"}
 | 
			
		||||
    ],
 | 
			
		||||
    entities: [],
 | 
			
		||||
    palette: [
 | 
			
		||||
        "minecraft:polished_andesite",
 | 
			
		||||
        "minecraft:light_gray_stained_glass",
 | 
			
		||||
        "minecraft:air",
 | 
			
		||||
        "computercraft:cable{cable:false,down:false,east:false,modem:up_off,north:false,south:false,up:false,waterlogged:false,west:false}"
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,138 @@
 | 
			
		||||
{
 | 
			
		||||
    DataVersion: 3465,
 | 
			
		||||
    size: [5, 5, 5],
 | 
			
		||||
    data: [
 | 
			
		||||
        {pos: [0, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 1], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [1, 1, 2], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [1, 1, 3], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [1, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 1], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [2, 1, 2], state: "computercraft:cable{cable:true,down:false,east:false,modem:up_off,north:false,south:false,up:true,waterlogged:false,west:false}", nbt: {id: "computercraft:cable"}},
 | 
			
		||||
        {pos: [2, 1, 3], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [2, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 1], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [3, 1, 2], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [3, 1, 3], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [3, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 1], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [1, 2, 2], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [1, 2, 3], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [1, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 1], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [2, 2, 2], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [2, 2, 3], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [2, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 1], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [3, 2, 2], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [3, 2, 3], state: "minecraft:light_gray_stained_glass"},
 | 
			
		||||
        {pos: [3, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 4], state: "minecraft:air"}
 | 
			
		||||
    ],
 | 
			
		||||
    entities: [],
 | 
			
		||||
    palette: [
 | 
			
		||||
        "minecraft:polished_andesite",
 | 
			
		||||
        "minecraft:light_gray_stained_glass",
 | 
			
		||||
        "minecraft:air",
 | 
			
		||||
        "computercraft:cable{cable:true,down:false,east:false,modem:up_off,north:false,south:false,up:true,waterlogged:false,west:false}"
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										138
									
								
								projects/common/src/testMod/resources/data/cctest/structures/turtle_test.sided_suck.snbt
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								projects/common/src/testMod/resources/data/cctest/structures/turtle_test.sided_suck.snbt
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,138 @@
 | 
			
		||||
{
 | 
			
		||||
    DataVersion: 3465,
 | 
			
		||||
    size: [5, 5, 5],
 | 
			
		||||
    data: [
 | 
			
		||||
        {pos: [0, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [1, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [2, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [3, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 0], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 1], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 2], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 3], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [4, 0, 4], state: "minecraft:polished_andesite"},
 | 
			
		||||
        {pos: [0, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 2], state: "computercraft:turtle_normal{facing:north,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [], Label: "turtle_test.sided_suck", On: 1b, Slot: 0, id: "computercraft:turtle_normal"}},
 | 
			
		||||
        {pos: [2, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 1, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 2], state: "minecraft:furnace{facing:north,lit:false}", nbt: {BurnTime: 0s, CookTime: 0s, CookTimeTotal: 200s, Items: [{Count: 64b, Slot: 1b, id: "minecraft:coal"}, {Count: 8b, Slot: 2b, id: "minecraft:iron_ingot"}], RecipesUsed: {"minecraft:iron_ingot_from_smelting_raw_iron": 8}, id: "minecraft:furnace"}},
 | 
			
		||||
        {pos: [2, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 2, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 3, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [0, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [1, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [2, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [3, 4, 4], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 0], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 1], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 2], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 3], state: "minecraft:air"},
 | 
			
		||||
        {pos: [4, 4, 4], state: "minecraft:air"}
 | 
			
		||||
    ],
 | 
			
		||||
    entities: [],
 | 
			
		||||
    palette: [
 | 
			
		||||
        "minecraft:polished_andesite",
 | 
			
		||||
        "minecraft:air",
 | 
			
		||||
        "computercraft:turtle_normal{facing:north,waterlogged:false}",
 | 
			
		||||
        "minecraft:furnace{facing:north,lit:false}"
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
@@ -116,6 +116,7 @@ public final class MethodResult {
 | 
			
		||||
     * @return The method result which represents this yield.
 | 
			
		||||
     * @see #pullEvent(String, ILuaCallback)
 | 
			
		||||
     */
 | 
			
		||||
    @SuppressWarnings("NamedLikeContextualKeyword")
 | 
			
		||||
    public static MethodResult yield(@Nullable Object[] arguments, ILuaCallback callback) {
 | 
			
		||||
        Objects.requireNonNull(callback, "callback cannot be null");
 | 
			
		||||
        return new MethodResult(arguments, callback);
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import dan200.computercraft.api.peripheral.NotAttachedException;
 | 
			
		||||
import dan200.computercraft.api.peripheral.WorkMonitor;
 | 
			
		||||
import dan200.computercraft.core.computer.ComputerSide;
 | 
			
		||||
import dan200.computercraft.core.computer.GuardedLuaContext;
 | 
			
		||||
import dan200.computercraft.core.methods.MethodSupplier;
 | 
			
		||||
import dan200.computercraft.core.methods.PeripheralMethod;
 | 
			
		||||
import dan200.computercraft.core.metrics.Metrics;
 | 
			
		||||
@@ -26,7 +27,7 @@ import java.util.*;
 | 
			
		||||
 * @hidden
 | 
			
		||||
 */
 | 
			
		||||
public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChangeListener {
 | 
			
		||||
    private class PeripheralWrapper extends ComputerAccess {
 | 
			
		||||
    private class PeripheralWrapper extends ComputerAccess implements GuardedLuaContext.Guard {
 | 
			
		||||
        private final String side;
 | 
			
		||||
        private final IPeripheral peripheral;
 | 
			
		||||
 | 
			
		||||
@@ -35,6 +36,8 @@ public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange
 | 
			
		||||
        private final Map<String, PeripheralMethod> methodMap;
 | 
			
		||||
        private boolean attached = false;
 | 
			
		||||
 | 
			
		||||
        private @Nullable GuardedLuaContext contextWrapper;
 | 
			
		||||
 | 
			
		||||
        PeripheralWrapper(IPeripheral peripheral, String side) {
 | 
			
		||||
            super(environment);
 | 
			
		||||
            this.side = side;
 | 
			
		||||
@@ -91,9 +94,21 @@ public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange
 | 
			
		||||
 | 
			
		||||
            if (method == null) throw new LuaException("No such method " + methodName);
 | 
			
		||||
 | 
			
		||||
            try (var ignored = environment.time(Metrics.PERIPHERAL_OPS)) {
 | 
			
		||||
                return method.apply(peripheral, context, this, arguments);
 | 
			
		||||
            // Wrap the ILuaContext. We try to reuse the previous context where possible to avoid allocations - this
 | 
			
		||||
            // should be pretty common as ILuaMachine uses a constant context.
 | 
			
		||||
            var contextWrapper = this.contextWrapper;
 | 
			
		||||
            if (contextWrapper == null || !contextWrapper.wraps(context)) {
 | 
			
		||||
                contextWrapper = this.contextWrapper = new GuardedLuaContext(context, this);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try (var ignored = environment.time(Metrics.PERIPHERAL_OPS)) {
 | 
			
		||||
                return method.apply(peripheral, contextWrapper, this, arguments);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public boolean checkValid() {
 | 
			
		||||
            return isAttached();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // IComputerAccess implementation
 | 
			
		||||
 
 | 
			
		||||
@@ -13,8 +13,54 @@ import dan200.computercraft.core.util.Colour;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Interact with a computer's terminal or monitors, writing text and drawing
 | 
			
		||||
 * ASCII graphics.
 | 
			
		||||
 * Interact with a computer's terminal or monitors, writing text and drawing ASCII graphics.
 | 
			
		||||
 *
 | 
			
		||||
 * <h2>Writing to the terminal</h2>
 | 
			
		||||
 * The simplest operation one can perform on a terminal is displaying (or writing) some text. This can be performed with
 | 
			
		||||
 * the [`term.write`] method.
 | 
			
		||||
 *
 | 
			
		||||
 * <pre>{@code
 | 
			
		||||
 * term.write("Hello, world!")
 | 
			
		||||
 * }</pre>
 | 
			
		||||
 * <p>
 | 
			
		||||
 * When you write text, this advances the cursor, so the next call to [`term.write`] will write text immediately after
 | 
			
		||||
 * the previous one.
 | 
			
		||||
 *
 | 
			
		||||
 * <pre>{@code
 | 
			
		||||
 * term.write("Hello, world!")
 | 
			
		||||
 * term.write("Some more text")
 | 
			
		||||
 * }</pre>
 | 
			
		||||
 * <p>
 | 
			
		||||
 * [`term.getCursorPos`] and [`term.setCursorPos`] can be used to manually change the cursor's position.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * <pre>{@code
 | 
			
		||||
 * term.clear()
 | 
			
		||||
 *
 | 
			
		||||
 * term.setCursorPos(1, 1) -- The first column of line 1
 | 
			
		||||
 * term.write("First line")
 | 
			
		||||
 *
 | 
			
		||||
 * term.setCursorPos(20, 2) -- The 20th column of line 2
 | 
			
		||||
 * term.write("Second line")
 | 
			
		||||
 * }</pre>
 | 
			
		||||
 * <p>
 | 
			
		||||
 * [`term.write`] is a relatively basic and low-level function, and does not handle more advanced features such as line
 | 
			
		||||
 * breaks or word wrapping. If you just want to display text to the screen, you probably want to use [`print`] or
 | 
			
		||||
 * [`write`] instead.
 | 
			
		||||
 *
 | 
			
		||||
 * <h2>Colours</h2>
 | 
			
		||||
 * So far we've been writing text in black and white. However, advanced computers are also capable of displaying text
 | 
			
		||||
 * in a variety of colours, with the [`term.setTextColour`] and [`term.setBackgroundColour`] functions.
 | 
			
		||||
 *
 | 
			
		||||
 * <pre>{@code
 | 
			
		||||
 * print("This text is white")
 | 
			
		||||
 * term.setTextColour(colours.green)
 | 
			
		||||
 * print("This text is green")
 | 
			
		||||
 * }</pre>
 | 
			
		||||
 * <p>
 | 
			
		||||
 * These functions accept any of the constants from the [`colors`] API. [Combinations of colours][`colors.combine`] may
 | 
			
		||||
 * be accepted, but will only display a single colour (typically following the behaviour of [`colors.toBlit`]).
 | 
			
		||||
 * <p>
 | 
			
		||||
 * The [`paintutils`] API provides several helpful functions for displaying graphics using [`term.setBackgroundColour`].
 | 
			
		||||
 *
 | 
			
		||||
 * @cc.module term
 | 
			
		||||
 */
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis.http.options;
 | 
			
		||||
 | 
			
		||||
import com.google.errorprone.annotations.Immutable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Options for a given HTTP request or websocket, which control its resource constraints.
 | 
			
		||||
@@ -14,5 +15,6 @@ package dan200.computercraft.core.apis.http.options;
 | 
			
		||||
 * @param websocketMessage The maximum size of a websocket message (outgoing and incoming).
 | 
			
		||||
 * @param useProxy         Whether to use the configured proxy.
 | 
			
		||||
 */
 | 
			
		||||
@Immutable
 | 
			
		||||
public record Options(Action action, long maxUpload, long maxDownload, int websocketMessage, boolean useProxy) {
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
package dan200.computercraft.core.apis.http.options;
 | 
			
		||||
 | 
			
		||||
import com.google.errorprone.annotations.Immutable;
 | 
			
		||||
import com.google.errorprone.annotations.concurrent.LazyInit;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
@@ -23,7 +24,7 @@ public final class PartialOptions {
 | 
			
		||||
    private final OptionalInt websocketMessage;
 | 
			
		||||
    private final Optional<Boolean> useProxy;
 | 
			
		||||
 | 
			
		||||
    @SuppressWarnings("Immutable") // Lazily initialised, so this mutation is invisible in the public API
 | 
			
		||||
    @LazyInit
 | 
			
		||||
    private @Nullable Options options;
 | 
			
		||||
 | 
			
		||||
    public PartialOptions(@Nullable Action action, OptionalLong maxUpload, OptionalLong maxDownload, OptionalInt websocketMessage, Optional<Boolean> useProxy) {
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ final class ResultHelpers {
 | 
			
		||||
        return throwUnchecked0(t);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @SuppressWarnings("unchecked")
 | 
			
		||||
    @SuppressWarnings({ "unchecked", "TypeParameterUnusedInFormals" })
 | 
			
		||||
    private static <T extends Throwable> T throwUnchecked0(Throwable t) throws T {
 | 
			
		||||
        throw (T) t;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -15,8 +15,7 @@ import dan200.computercraft.core.filesystem.FileSystem;
 | 
			
		||||
import dan200.computercraft.core.terminal.Terminal;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
import java.util.concurrent.atomic.AtomicBoolean;
 | 
			
		||||
import java.util.concurrent.atomic.AtomicInteger;
 | 
			
		||||
import java.util.concurrent.atomic.AtomicLong;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -54,9 +53,9 @@ public class Computer {
 | 
			
		||||
    private final AtomicLong lastTaskId = new AtomicLong();
 | 
			
		||||
 | 
			
		||||
    // Additional state about the computer and its environment.
 | 
			
		||||
    private boolean blinking = false;
 | 
			
		||||
    private final Environment internalEnvironment;
 | 
			
		||||
    private final AtomicBoolean externalOutputChanged = new AtomicBoolean();
 | 
			
		||||
 | 
			
		||||
    private final AtomicInteger externalOutputChanges = new AtomicInteger();
 | 
			
		||||
 | 
			
		||||
    private boolean startRequested;
 | 
			
		||||
    private int ticksSinceStart = -1;
 | 
			
		||||
@@ -140,10 +139,7 @@ public class Computer {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setLabel(@Nullable String label) {
 | 
			
		||||
        if (!Objects.equals(label, this.label)) {
 | 
			
		||||
        this.label = label;
 | 
			
		||||
            externalOutputChanged.set(true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void tick() {
 | 
			
		||||
@@ -164,28 +160,24 @@ public class Computer {
 | 
			
		||||
        internalEnvironment.tick();
 | 
			
		||||
 | 
			
		||||
        // Propagate the environment's output to the world.
 | 
			
		||||
        if (internalEnvironment.updateOutput()) externalOutputChanged.set(true);
 | 
			
		||||
 | 
			
		||||
        // Set output changed if the terminal has changed from blinking to not
 | 
			
		||||
        var blinking = terminal.getCursorBlink() &&
 | 
			
		||||
            terminal.getCursorX() >= 0 && terminal.getCursorX() < terminal.getWidth() &&
 | 
			
		||||
            terminal.getCursorY() >= 0 && terminal.getCursorY() < terminal.getHeight();
 | 
			
		||||
        if (blinking != this.blinking) {
 | 
			
		||||
            this.blinking = blinking;
 | 
			
		||||
            externalOutputChanged.set(true);
 | 
			
		||||
        }
 | 
			
		||||
        externalOutputChanges.accumulateAndGet(internalEnvironment.updateOutput(), (x, y) -> x | y);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void markChanged() {
 | 
			
		||||
        externalOutputChanged.set(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean pollAndResetChanged() {
 | 
			
		||||
        return externalOutputChanged.getAndSet(false);
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a bitmask returning which sides on the computer have changed, resetting the internal state.
 | 
			
		||||
     *
 | 
			
		||||
     * @return What sides on the computer have changed.
 | 
			
		||||
     */
 | 
			
		||||
    public int pollAndResetChanges() {
 | 
			
		||||
        return externalOutputChanges.getAndSet(0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean isBlinking() {
 | 
			
		||||
        return isOn() && blinking;
 | 
			
		||||
        if (!isOn() || !terminal.getCursorBlink()) return false;
 | 
			
		||||
 | 
			
		||||
        var cursorX = terminal.getCursorX();
 | 
			
		||||
        var cursorY = terminal.getCursorY();
 | 
			
		||||
        return cursorX >= 0 && cursorX < terminal.getWidth() && cursorY >= 0 && cursorY < terminal.getHeight();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void addApi(ILuaAPI api) {
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user