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

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

1
.gitignore vendored
View File

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

View File

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

View File

@ -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"

View File

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

View File

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

View File

@ -19,7 +19,7 @@ # Setting up GPS
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

140
doc/reference/command.md Normal file
View File

@ -0,0 +1,140 @@
---
module: [kind=reference] computercraft_command
---
<!--
SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0
-->
# The `/computercraft` command
CC: Tweaked provides a `/computercraft` command for server owners to manage running computers on a server.
## Permissions {#permissions}
As the `/computercraft` command is mostly intended for debugging and administrative purposes, its sub-commands typically
require you to have op (or similar).
- All players have access to the [`queue`] sub-command.
- On a multi-player server, all other commands require op.
- On a single-player world, the player can run the [`dump`], [`turn-on`]/[`shutdown`], and [`track`] sub-commands, even
when cheats are not enabled. The [`tp`] and [`view`] commands require cheats.
If a permission mod such as [LuckPerms] is installed[^permission], you can configure access to the individual
sub-commands. Each sub-command creates a `computercraft.command.NAME` permission node to control which players can
execute it.
[LuckPerms]: https://github.com/LuckPerms/LuckPerms/ "A permissions plugin for Minecraft servers."
[fabric-permission-api]: https://github.com/lucko/fabric-permissions-api "A simple permissions API for Fabric"
[^permission]: This supports any mod which uses Forge's permission API or [fabric-permission-api].
## Computer selectors {#computer-selectors}
Some commands (such as [`tp`] or [`turn-on`]) target a specific computer, or a list of computers. To specify which
computers to operate on, you must use "computer selectors".
Computer selectors are similar to Minecraft's [entity target selectors], but targeting computers instead. They allow
you to select one or more computers, based on a set of predicates.
The following predicates are supported:
- `id=<id>`: Select computer(s) with a specific id.
- `instance=<id>`: Select the computer with the given instance id.
- `family=<normal|advanced|command>`: Select computers based on their type.
- `label=<label>`: Select computers with the given label.
- `distance=<distance>`: Select computers within a specific distance of the player executing the command. This uses
Minecraft's [float range] syntax.
`#<id>` may also be used as a shorthand for `@c[id=<id>]`, to select computer(s) with a specific id.
### Examples:
- `/computercraft turn-on #12`: Turn on the computer(s) with an id of 12.
- `/computercraft shutdown @c[distance=..100]`: Shut down all computers with 100 blocks of the player.
[entity target selectors]: https://minecraft.wiki/w/Target_selectors "Target Selectors on the Minecraft wiki"
[Float range]: https://minecraft.wiki/w/Argument_types#minecraft:float_range
## Commands {#commands}
### `/computercraft dump` {#dump}
`/computercraft dump` prints a table of currently loaded computers, including their id, position, and whether they're
running. It can also be run with a single computer argument to dump more detailed information about a computer.
![A screenshot of a Minecraft world. In the chat box, there is a table listing 5 computers, with columns labelled
"Computer", "On" and "Position". Below that, is a more detailed list of information about Computer 0, including its
label ("My computer") and that it has a monitor on the right hand side](../images/computercraft-dump.png "An example of
running '/computercraft dump'")
Next to the computer id, there are several buttons to either [teleport][`tp`] to the computer, or [open its terminal
][`view`].
Computers are sorted by distance to the player, so nearby computers will appear earlier.
### `/computercraft turn-on [computers...]` {#turn-on}
Turn on one or more computers or, if no run with no arguments, all loaded computers.
#### Examples
- `/computercraft turn-on #0 #2`: Turn on computers with id 0 and 2.
- `/computercraft turn-on @c[family=command]`: Turn on all command computers.
### `/computercraft shutdown [computers...]` {#shutdown}
Shutdown one or more computers or, if no run with no arguments, all loaded computers.
This is sometimes useful when dealing with lag, as a way to ensure that ComputerCraft is not causing problems.
#### Examples
- `/computercraft shutdown`: Shut down all loaded computers.
- `/computercraft shutdown @c[distance=..10]`: Shut down all computers in a block radius.
### `/computercraft tp [computer]` {#tp}
Teleport to the given computer.
This is normally used from via the [`dump`] command interface rather than being invoked directly.
### `/computercraft view [computer]` {#view}
Open a terminal for the specified computer. This allows remotely viewing computers without having to interact with the
block.
This is normally used from via the [`dump`] command interface rather than being invoked directly.
### `/computercraft track` {#track}
The `/computercraft track` command allows you to enable profiling of computers. When a computer runs code, or interacts
with the Minecraft world, we time how long that takes. This timing information may then be queried, and used to find
computers which may be causing lag.
To enable the profiler, run `/computercraft track start`. Computers will then start recording metrics. Once enough data
has been gathered, run `/computercraft track stop` to stop profiling and display the recorded data.
![](../images/computercraft-track.png)
The table by default shows the number of times each computer has run, and how long it ran for (in total, and on
average). In the above screenshot, we can see one computer was particularly badly behaved, and ran for 7 seconds. The
buttons may be used to [teleport][`tp`] to the computer, or [open its terminal ][`view`], and inspect it further.
`/computercraft track dump` can be used to display this table at any point (including while profiling is still running).
Computers also record other information, such as how much server-thread time they consume, or their HTTP bandwidth
usage. The `dump` subcommand accepts a list of other fields to display, instead of the default timings.
#### Examples
- `/computercraft track dump server_tasks_count server_tasks`: Print the number of server-thread tasks each computer
executed, and how long they took in total.
- `/computercraft track dump http_upload http_download`: Print the number of bytes uploaded and downloaded by each
computer.
### `/computercraft queue` {#queue}
The queue subcommand allows non-operator players to queue a `computer_command` event on *command* computers.
This has a similar purpose to vanilla's [`/trigger`] command. Command computers may choose to listen to this event, and
then perform some action.
[`/trigger`]: https://minecraft.wiki/w/Commands/trigger "/trigger on the Minecraft wiki"
[`dump`]: #dump "/computercraft dump"
[`queue`]: #queue "/computercraft queue"
[`shutdown`]: #shutdown "/computercraft shutdown"
[`tp`]: #tp "/computercraft tp"
[`track`]: #track "/computercraft track"
[`turn-on`]: #turn-on "/computercraft turn-on"
[`view`]: #view "/computercraft view"
[computer selectors]: #computer-selectors "Computer selectors"

View File

@ -10,7 +10,7 @@ kotlin.jvm.target.validation.mode=error
# Mod properties
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

View File

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

View File

@ -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
View File

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

View File

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

View File

@ -6,6 +6,7 @@
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 @@
* 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 @@ default boolean connectTo(WiredNode node) {
*
* @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 @@ default boolean disconnectFrom(WiredNode node) {
* @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 @@ default boolean remove() {
* 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);
}

View File

@ -29,7 +29,7 @@ private 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());

View File

@ -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")))

View File

@ -22,6 +22,7 @@
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 static void register() {
/**
* 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 static void registerTurtleModellers(RegisterTurtleUpgradeModeller registe
}
@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 static void registerItemColours(BiConsumer<ItemColor, ItemLike> register)
}
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) {

View File

@ -17,6 +17,7 @@
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.level.Level;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.UUID;
/**
@ -67,19 +67,17 @@ public void handlePlayRecord(BlockPos pos, @Nullable SoundEvent sound, @Nullable
}
@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);
}

View File

@ -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 static void reset() {
instances.clear();
}
public static void remove(int id) {
public static void remove(UUID id) {
instances.remove(id);
}
/**
* Get or create a pocket computer.
* Set the state of a pocket computer.
*
* @param instanceId The instance ID of the pocket computer.
* @param advanced Whether this computer has an advanced terminal.
* @return The pocket computer data.
* @param instanceId The instance ID of the pocket computer.
* @param state The computer state of the pocket computer.
* @param lightColour The current colour of the modem light.
* @param terminalData The current terminal contents.
*/
public static PocketComputerData get(int instanceId, boolean advanced) {
public static void setState(UUID instanceId, ComputerState state, int lightColour, TerminalState terminalData) {
var computer = instances.get(instanceId);
if (computer == null) instances.put(instanceId, computer = new PocketComputerData(advanced));
return computer;
if (computer == null) {
var terminal = new NetworkedTerminal(terminalData.width, terminalData.height, terminalData.colour);
instances.put(instanceId, computer = new PocketComputerData(state, lightColour, terminal));
} else {
computer.setState(state, lightColour);
}
if (terminalData.hasTerminal()) terminalData.apply(computer.getTerminal());
}
public static PocketComputerData get(ItemStack stack) {
var family = stack.getItem() instanceof PocketComputerItem computer ? computer.getFamily() : ComputerFamily.NORMAL;
return get(PocketComputerItem.getInstanceID(stack), family != ComputerFamily.NORMAL);
public static @Nullable PocketComputerData get(ItemStack stack) {
var id = PocketComputerItem.getInstanceID(stack);
return id == null ? null : instances.get(id);
}
}

View File

@ -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 @@
* @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 ComputerState getState() {
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);
}
}

View File

@ -11,6 +11,7 @@
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 @@ private PocketItemRenderer() {
@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 @@ protected void renderItem(PoseStack transform, MultiBufferSource bufferSource, I
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();
}

View File

@ -60,9 +60,9 @@ public MonitorBlockEntityRenderer(BlockEntityRendererProvider.Context context) {
@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();

View File

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

View File

@ -6,12 +6,12 @@
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 @@ private void pushAudio(ByteBuffer buffer) {
}
}
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();

View File

@ -13,6 +13,7 @@
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 @@ private void addTranslations() {
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 @@ private Stream<String> getExpectedKeys() {
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);
}

View File

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

View File

@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.impl.network.wired;
import dan200.computercraft.api.network.wired.WiredNode;
import javax.annotation.Nullable;
import java.util.Objects;
/**
* A disjoint-set/union-find of {@link WiredNodeImpl}s.
* <p>
* Rather than actually maintaining a list of included nodes, wired nodes store {@linkplain WiredNodeImpl#currentSet the
* set they're part of}. This means that we can only have one disjoint-set at once, but that is not a problem in
* practice.
*
* @see WiredNodeImpl#currentSet
* @see WiredNetworkImpl#remove(WiredNode)
* @see <a href="https://en.wikipedia.org/wiki/Disjoint-set_data_structure">Disjoint-set data structure</a>
*/
class NodeSet {
private NodeSet parent = this;
private int size = 1;
private @Nullable WiredNetworkImpl network;
private boolean isRoot() {
return parent == this;
}
/**
* Resolve this union, finding the root {@link NodeSet}.
*
* @return The root union.
*/
NodeSet find() {
var self = this;
while (!self.isRoot()) self = self.parent = self.parent.parent;
return self;
}
/**
* Get the size of this node set.
*
* @return The size of the set.
*/
int size() {
return find().size;
}
/**
* Add a node to this {@link NodeSet}.
*
* @param node The node to add to the set.
*/
void addNode(WiredNodeImpl node) {
if (!isRoot()) throw new IllegalStateException("Cannot grow a non-root set.");
if (node.currentSet != null) throw new IllegalArgumentException("Node is already in a set.");
node.currentSet = this;
size++;
}
/**
* Merge two nodes sets together.
*
* @param left The first union.
* @param right The second union.
* @return The union which was subsumed.
*/
public static NodeSet merge(NodeSet left, NodeSet right) {
if (!left.isRoot() || !right.isRoot()) throw new IllegalArgumentException("Cannot union a non-root set.");
if (left == right) throw new IllegalArgumentException("Cannot merge a node into itself.");
return left.size >= right.size ? mergeInto(left, right) : mergeInto(right, left);
}
private static NodeSet mergeInto(NodeSet root, NodeSet child) {
assert root.size > child.size;
child.parent = root;
root.size += child.size;
return child;
}
void setNetwork(WiredNetworkImpl network) {
if (!isRoot()) throw new IllegalStateException("Set is not the root.");
if (this.network != null) throw new IllegalStateException("Set already has a network.");
this.network = network;
}
/**
* Get the associated network.
*
* @return The associated network.
*/
WiredNetworkImpl network() {
return Objects.requireNonNull(find().network);
}
}

View File

@ -6,6 +6,7 @@
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 @@ static WiredNetworkChangeImpl changeOf(Map<String, IPeripheral> oldPeripherals,
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);

View File

@ -8,6 +8,7 @@
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 @@ public boolean remove(WiredNode node) {
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 @@ public boolean remove(WiredNode node) {
return true;
}
// A split may cause 2..neighbours.size() separate networks, so we
// iterate through our neighbour list, generating child networks.
neighbours.removeAll(reachable);
var maximals = new ArrayList<WiredNetworkImpl>(neighbours.size() + 1);
maximals.add(wiredNetwork);
maximals.add(new WiredNetworkImpl(reachable));
assert queue.size() == nodes.size() : "Expected queue to contain all nodes.";
while (!neighbours.isEmpty()) {
reachable = reachableNodes(neighbours.iterator().next());
neighbours.removeAll(reachable);
maximals.add(new WiredNetworkImpl(reachable));
// Otherwise we need to create our new networks.
var networks = new ArrayList<WiredNetworkImpl>(1 + nodeSets.size());
// Add the network we've created for the removed node.
networks.add(wiredNetwork);
// And then create a new network for each disjoint subset.
for (var set : nodeSets) {
var network = new WiredNetworkImpl(new HashSet<>(set.size()));
set.setNetwork(network);
networks.add(network);
}
for (var network : maximals) network.lock.writeLock().lock();
for (var network : networks) network.lock.writeLock().lock();
try {
// We special case the original node: detaching all peripherals when needed.
wired.network = wiredNetwork;
wired.peripherals = Map.of();
wired.neighbours.clear();
// Ensure every network is finalised
for (var network : maximals) {
for (var child : network.nodes) {
child.network = network;
network.peripherals.putAll(child.peripherals);
}
// Add all nodes to their appropriate network.
for (var child : queue) {
var network = Nullability.assertNonNull(child.currentSet).network();
child.currentSet = null;
child.network = network;
network.nodes.add(child);
network.peripherals.putAll(child.peripherals);
}
for (var network : maximals) InvariantChecker.checkNetwork(network);
for (var network : networks) InvariantChecker.checkNetwork(network);
InvariantChecker.checkNode(wired);
// Then broadcast network changes once all nodes are finalised
for (var network : maximals) {
for (var network : networks) {
WiredNetworkChangeImpl.changeOf(peripherals, network.peripherals).broadcast(network.nodes);
}
} finally {
for (var network : maximals) network.lock.writeLock().unlock();
for (var network : networks) network.lock.writeLock().unlock();
}
nodes.clear();
@ -373,22 +443,4 @@ private static WiredNodeImpl checkNode(WiredNode node) {
throw new IllegalArgumentException("Unknown implementation of IWiredNode: " + node);
}
}
private static Set<WiredNodeImpl> reachableNodes(WiredNodeImpl start) {
Queue<WiredNodeImpl> enqueued = new ArrayDeque<>();
var reachable = new HashSet<WiredNodeImpl>();
reachable.add(start);
enqueued.add(start);
WiredNodeImpl node;
while ((node = enqueued.poll()) != null) {
for (var neighbour : node.neighbours) {
// Otherwise attempt to enqueue this neighbour as well.
if (reachable.add(neighbour)) enqueued.add(neighbour);
}
}
return reachable;
}
}

View File

@ -27,11 +27,39 @@ public final class WiredNodeImpl implements WiredNode {
final HashSet<WiredNodeImpl> neighbours = new HashSet<>();
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<>();

View File

@ -18,6 +18,7 @@
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 @@ private static void resetState() {
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(

View File

@ -19,7 +19,6 @@
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 @@ private static <T extends ArgumentType<?>> void register(String name, Class<T> t
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());
}
}

View File

@ -12,7 +12,8 @@
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.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 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 static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
.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 @@ private static int dump(CommandSourceStack source) {
} 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 @@ private static int dump(CommandSourceStack source) {
*/
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 @@ private static int trackDump(CommandSourceStack source, List<AggregatedMetric> f
// 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 @@ private static Component linkComputer(CommandSourceStack source, @Nullable Serve
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 @@ private static Component linkPosition(CommandSourceStack context, ServerComputer
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 @@ private static Component linkPosition(CommandSourceStack context, ServerComputer
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 @@ private static int displayTimings(CommandSourceStack source, List<ComputerMetric
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;
}
}

View File

@ -28,12 +28,12 @@ public static boolean isPlayer(CommandSourceStack output) {
@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);
}
}

View File

@ -7,6 +7,8 @@
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));
}

View File

@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.command.arguments;
import com.mojang.brigadier.StringReader;
final class ArgumentParserUtils {
private ArgumentParserUtils() {
}
public static boolean consume(StringReader reader, char lookahead) {
if (!reader.canRead() || reader.peek() != lookahead) return false;
reader.skip();
return true;
}
public static boolean consume(StringReader reader, String lookahead) {
if (!reader.canRead(lookahead.length())) return false;
for (var i = 0; i < lookahead.length(); i++) {
if (reader.peek(i) != lookahead.charAt(i)) return false;
}
reader.setCursor(reader.getCursor() + lookahead.length());
return true;
}
}

View File

@ -14,66 +14,58 @@
import 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;
}
}

View File

@ -0,0 +1,387 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.command.arguments;
import com.mojang.brigadier.Message;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.core.ServerContext;
import net.minecraft.advancements.critereon.MinMaxBounds;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.commands.arguments.UuidArgument;
import net.minecraft.network.chat.Component;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import javax.annotation.Nullable;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static dan200.computercraft.shared.command.CommandUtils.suggestOnServer;
import static dan200.computercraft.shared.command.Exceptions.*;
import static dan200.computercraft.shared.command.arguments.ArgumentParserUtils.consume;
import static dan200.computercraft.shared.command.text.ChatHelpers.makeComputerDumpCommand;
public record ComputerSelector(
String selector,
OptionalInt instanceId,
@Nullable UUID instanceUuid,
OptionalInt computerId,
@Nullable String label,
@Nullable ComputerFamily family,
@Nullable AABB bounds,
@Nullable MinMaxBounds.Doubles range
) {
private static final ComputerSelector all = new ComputerSelector("@c[]", OptionalInt.empty(), null, OptionalInt.empty(), null, null, null, null);
private static UuidArgument uuidArgument = UuidArgument.uuid();
/**
* A {@link ComputerSelector} which matches all computers.
*
* @return A {@link ComputerSelector} instance.
*/
public static ComputerSelector all() {
return all;
}
/**
* Find all computers matching this selector.
*
* @param source The source requesting these computers.
* @return The stream of matching computers.
*/
public Stream<ServerComputer> find(CommandSourceStack source) {
var context = ServerContext.get(source.getServer());
if (instanceId().isPresent()) {
var computer = context.registry().get(instanceId().getAsInt());
return computer != null && matches(source, computer) ? Stream.of(computer) : Stream.of();
}
if (instanceUuid() != null) {
var computer = context.registry().get(instanceUuid());
return computer != null && matches(source, computer) ? Stream.of(computer) : Stream.of();
}
return context.registry().getComputers().stream().filter(c -> matches(source, c));
}
/**
* Find exactly one computer which matches this selector.
*
* @param source The source requesting this computer.
* @return The computer.
* @throws CommandSyntaxException If no or multiple computers could be found.
*/
public ServerComputer findOne(CommandSourceStack source) throws CommandSyntaxException {
var computers = find(source).toList();
if (computers.isEmpty()) throw COMPUTER_ARG_NONE.create(selector);
if (computers.size() == 1) return computers.iterator().next();
var builder = Component.empty();
var first = true;
for (var computer : computers) {
if (first) {
first = false;
} else {
builder.append(", ");
}
builder.append(makeComputerDumpCommand(computer));
}
// We have an incorrect number of computers: throw an error
throw COMPUTER_ARG_MANY.create(selector, builder);
}
/**
* Determine if this selector matches a given computer.
*
* @param source The command source, used for distance comparisons.
* @param computer The computer to check.
* @return If this computer is matched by the selector.
*/
public boolean matches(CommandSourceStack source, ServerComputer computer) {
return (instanceId().isEmpty() || computer.getInstanceID() == instanceId().getAsInt())
&& (instanceUuid() == null || computer.getInstanceUUID().equals(instanceUuid()))
&& (computerId().isEmpty() || computer.getID() == computerId().getAsInt())
&& (label == null || Objects.equals(computer.getLabel(), label))
&& (family == null || computer.getFamily() == family)
&& (bounds == null || (source.getLevel() == computer.getLevel() && bounds.contains(Vec3.atCenterOf(computer.getPosition()))))
&& (range == null || (source.getLevel() == computer.getLevel() && range.matchesSqr(source.getPosition().distanceToSqr(Vec3.atCenterOf(computer.getPosition())))));
}
/**
* Parse an input string.
*
* @param reader The reader to parse from.
* @return The parsed selector.
* @throws CommandSyntaxException If the selector was incomplete or malformed.
*/
public static ComputerSelector parse(StringReader reader) throws CommandSyntaxException {
var start = reader.getCursor();
var builder = new Builder();
if (consume(reader, "@c[")) {
parseSelector(builder, reader);
} else {
// TODO(1.20.5): Only parse computer ids here.
var kind = reader.peek();
if (kind == '@') {
reader.skip();
builder.label = reader.readString();
} else if (kind == '~') {
reader.skip();
builder.family = parseFamily(reader);
} else if (kind == '#') {
reader.skip();
builder.computerId = OptionalInt.of(reader.readInt());
} else {
builder.instanceId = OptionalInt.of(reader.readInt());
}
}
var selector = reader.getString().substring(start, reader.getCursor());
return new ComputerSelector(selector, builder.instanceId, builder.instanceUuid, builder.computerId, builder.label, builder.family, builder.bounds, builder.range);
}
private static void parseSelector(Builder builder, StringReader reader) throws CommandSyntaxException {
Set<Option> seenOptions = new HashSet<>();
while (true) {
reader.skipWhitespace();
if (!reader.canRead()) throw ERROR_EXPECTED_END_OF_OPTIONS.createWithContext(reader);
if (consume(reader, ']')) break;
// Read the option and validate it.
var option = parseOption(reader, seenOptions);
reader.skipWhitespace();
if (!consume(reader, '=')) throw ERROR_EXPECTED_OPTION_VALUE.createWithContext(reader, option.name());
reader.skipWhitespace();
option.parser.parse(reader, builder);
reader.skipWhitespace();
if (consume(reader, ']')) break;
if (!consume(reader, ',')) throw ERROR_EXPECTED_END_OF_OPTIONS.createWithContext(reader);
}
}
private static Option parseOption(StringReader reader, Set<Option> seen) throws CommandSyntaxException {
var start = reader.getCursor();
var name = reader.readUnquotedString();
var option = options.get(name);
if (option == null) {
reader.setCursor(start);
throw ERROR_UNKNOWN_OPTION.createWithContext(reader, name);
} else if (!seen.add(option)) {
throw ERROR_INAPPLICABLE_OPTION.createWithContext(reader, name);
}
return option;
}
private static ComputerFamily parseFamily(StringReader reader) throws CommandSyntaxException {
var start = reader.getCursor();
var name = reader.readUnquotedString();
var family = Arrays.stream(ComputerFamily.values()).filter(x -> x.name().equalsIgnoreCase(name)).findFirst().orElse(null);
if (family == null) {
reader.setCursor(start);
throw UNKNOWN_FAMILY.createWithContext(reader, name);
}
return family;
}
/**
* Suggest completions for a selector argument.
*
* @param context The current command context.
* @param builder The builder containing the current input.
* @return The possible suggestions.
*/
public static CompletableFuture<Suggestions> suggest(CommandContext<?> context, SuggestionsBuilder builder) {
var remaining = builder.getRemaining();
if (remaining.startsWith("@")) {
var reader = new StringReader(builder.getInput());
reader.setCursor(builder.getStart());
return suggestSelector(context, reader);
} else if (remaining.startsWith("#")) {
return suggestComputers(c -> "#" + c.getID()).suggest(context, builder);
} else {
return suggestComputers(c -> Integer.toString(c.getInstanceID())).suggest(context, builder);
}
}
private static CompletableFuture<Suggestions> suggestSelector(CommandContext<?> context, StringReader reader) {
Set<Option> seenOptions = new HashSet<>();
var builder = new Builder();
if (!consume(reader, "@c[")) return suggestions(reader).suggest("@c[").buildFuture();
while (true) {
reader.skipWhitespace();
if (!reader.canRead()) return suggestOptions(reader);
if (consume(reader, ']')) break;
// Read the option and validate it.
Option option;
try {
option = parseOption(reader, seenOptions);
} catch (CommandSyntaxException e) {
return suggestOptions(reader);
}
reader.skipWhitespace();
if (!consume(reader, '=')) return suggestions(reader).suggest("=").buildFuture();
reader.skipWhitespace();
try {
option.parser.parse(reader, builder);
} catch (CommandSyntaxException e) {
return option.suggest.suggest(context, suggestions(reader));
}
reader.skipWhitespace();
if (consume(reader, ']')) break;
if (!consume(reader, ',')) return suggestions(reader).suggest(",").buildFuture();
}
return Suggestions.empty();
}
private static CompletableFuture<Suggestions> suggestOptions(StringReader reader) {
return SharedSuggestionProvider.suggest(options().values(), suggestions(reader), Option::name, Option::tooltip);
}
private static SuggestionsBuilder suggestions(StringReader reader) {
return new SuggestionsBuilder(reader.getString(), reader.getCursor());
}
private static final class Builder {
private OptionalInt instanceId = OptionalInt.empty();
private @Nullable UUID instanceUuid = null;
private OptionalInt computerId = OptionalInt.empty();
private @Nullable String label;
private @Nullable ComputerFamily family;
private @Nullable AABB bounds;
private @Nullable MinMaxBounds.Doubles range;
}
private static final Map<String, Option> options;
/**
* Get a map of individual selector options.
*
* @return The available options.
*/
public static Map<String, Option> options() {
return options;
}
static {
var optionList = new Option[]{
new Option(
"instance",
(reader, builder) -> builder.instanceUuid = uuidArgument.parse(reader),
suggestComputers(c -> c.getInstanceUUID().toString())
),
new Option(
"id",
(reader, builder) -> builder.computerId = OptionalInt.of(reader.readInt()),
suggestComputers(c -> Integer.toString(c.getID()))
),
new Option(
"label",
(reader, builder) -> builder.label = reader.readQuotedString(),
suggestComputers(ServerComputer::getLabel)
),
new Option(
"family",
(reader, builder) -> builder.family = parseFamily(reader),
(source, builder) -> SharedSuggestionProvider.suggest(Arrays.stream(ComputerFamily.values()).map(x -> x.name().toLowerCase(Locale.ROOT)), builder)
),
new Option(
"distance",
(reader, builder) -> builder.range = MinMaxBounds.Doubles.fromReader(reader),
(source, builder) -> Suggestions.empty()
),
};
options = Arrays.stream(optionList).collect(Collectors.toUnmodifiableMap(Option::name, x -> x));
}
/**
* A single option to filter a computer by.
*/
public static final class Option {
private final String name;
private final Parser parser;
private final SuggestionProvider suggest;
private final String translationKey;
private final Message tooltip;
Option(String name, Parser parser, SuggestionProvider suggest) {
this.name = name;
this.parser = parser;
this.suggest = suggest;
tooltip = Component.translatable(translationKey = "argument.computercraft.computer." + name);
}
/**
* The name of this selector.
*
* @return The selector's name.
*/
public String name() {
return name;
}
public Message tooltip() {
return tooltip;
}
/**
* The translation key for this selector.
*
* @return The selector's translation key.
*/
public String translationKey() {
return translationKey;
}
}
private interface Parser {
void parse(StringReader reader, Builder builder) throws CommandSyntaxException;
}
private interface SuggestionProvider {
CompletableFuture<Suggestions> suggest(CommandContext<?> source, SuggestionsBuilder builder);
}
private static SuggestionProvider suggestComputers(Function<ServerComputer, String> renderer) {
return (anyContext, builder) -> suggestOnServer(anyContext, context -> {
var remaining = builder.getRemaining();
for (var computer : ServerContext.get(context.getSource().getServer()).registry().getComputers()) {
var converted = renderer.apply(computer);
if (converted != null && converted.startsWith(remaining)) {
builder.suggest(converted);
}
}
return builder.buildFuture();
});
}
}

View File

@ -1,188 +0,0 @@
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.command.arguments;
import com.google.gson.JsonObject;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.core.ServerContext;
import net.minecraft.commands.CommandBuildContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
import net.minecraft.network.FriendlyByteBuf;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.function.Predicate;
import static dan200.computercraft.shared.command.CommandUtils.suggest;
import static dan200.computercraft.shared.command.CommandUtils.suggestOnServer;
import static dan200.computercraft.shared.command.Exceptions.COMPUTER_ARG_NONE;
public final class ComputersArgumentType implements ArgumentType<ComputersArgumentType.ComputersSupplier> {
private static final ComputersArgumentType MANY = new ComputersArgumentType(false);
private static final ComputersArgumentType SOME = new ComputersArgumentType(true);
private static final List<String> EXAMPLES = List.of(
"0", "#0", "@Label", "~Advanced"
);
public static ComputersArgumentType manyComputers() {
return MANY;
}
public static ComputersArgumentType someComputers() {
return SOME;
}
public static Collection<ServerComputer> getComputersArgument(CommandContext<CommandSourceStack> context, String name) throws CommandSyntaxException {
return context.getArgument(name, ComputersSupplier.class).unwrap(context.getSource());
}
private final boolean requireSome;
private ComputersArgumentType(boolean requireSome) {
this.requireSome = requireSome;
}
@Override
public ComputersSupplier parse(StringReader reader) throws CommandSyntaxException {
var start = reader.getCursor();
var kind = reader.peek();
ComputersSupplier computers;
if (kind == '@') {
reader.skip();
var label = reader.readUnquotedString();
computers = getComputers(x -> Objects.equals(label, x.getLabel()));
} else if (kind == '~') {
reader.skip();
var family = reader.readUnquotedString();
computers = getComputers(x -> x.getFamily().name().equalsIgnoreCase(family));
} else if (kind == '#') {
reader.skip();
var id = reader.readInt();
computers = getComputers(x -> x.getID() == id);
} else {
var instance = reader.readInt();
computers = s -> {
var computer = ServerContext.get(s.getServer()).registry().get(instance);
return computer == null ? List.of() : List.of(computer);
};
}
if (requireSome) {
var selector = reader.getString().substring(start, reader.getCursor());
return source -> {
var matched = computers.unwrap(source);
if (matched.isEmpty()) throw COMPUTER_ARG_NONE.create(selector);
return matched;
};
} else {
return computers;
}
}
@Override
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
var remaining = builder.getRemaining();
// We can run this one on the client, for obvious reasons.
if (remaining.startsWith("~")) {
return suggest(builder, ComputerFamily.values(), x -> "~" + x.name());
}
// Verify we've a command source and we're running on the server
return suggestOnServer(context, s -> {
if (remaining.startsWith("@")) {
suggestComputers(s.getSource(), builder, remaining, x -> {
var label = x.getLabel();
return label == null ? null : "@" + label;
});
} else if (remaining.startsWith("#")) {
suggestComputers(s.getSource(), builder, remaining, c -> "#" + c.getID());
} else {
suggestComputers(s.getSource(), builder, remaining, c -> Integer.toString(c.getInstanceID()));
}
return builder.buildFuture();
});
}
@Override
public Collection<String> getExamples() {
return EXAMPLES;
}
private static void suggestComputers(CommandSourceStack source, SuggestionsBuilder builder, String remaining, Function<ServerComputer, String> renderer) {
remaining = remaining.toLowerCase(Locale.ROOT);
for (var computer : ServerContext.get(source.getServer()).registry().getComputers()) {
var converted = renderer.apply(computer);
if (converted != null && converted.toLowerCase(Locale.ROOT).startsWith(remaining)) {
builder.suggest(converted);
}
}
}
private static ComputersSupplier getComputers(Predicate<ServerComputer> predicate) {
return s -> ServerContext.get(s.getServer()).registry()
.getComputers()
.stream()
.filter(predicate)
.toList();
}
public static class Info implements ArgumentTypeInfo<ComputersArgumentType, Template> {
@Override
public void serializeToNetwork(ComputersArgumentType.Template arg, FriendlyByteBuf buf) {
buf.writeBoolean(arg.requireSome());
}
@Override
public ComputersArgumentType.Template deserializeFromNetwork(FriendlyByteBuf buf) {
var requiresSome = buf.readBoolean();
return new ComputersArgumentType.Template(this, requiresSome);
}
@Override
public void serializeToJson(ComputersArgumentType.Template arg, JsonObject json) {
json.addProperty("requireSome", arg.requireSome);
}
@Override
public ComputersArgumentType.Template unpack(ComputersArgumentType argumentType) {
return new ComputersArgumentType.Template(this, argumentType.requireSome);
}
}
public record Template(Info info, boolean requireSome) implements ArgumentTypeInfo.Template<ComputersArgumentType> {
@Override
public ComputersArgumentType instantiate(CommandBuildContext context) {
return requireSome ? SOME : MANY;
}
@Override
public Info type() {
return info;
}
}
@FunctionalInterface
public interface ComputersSupplier {
Collection<ServerComputer> unwrap(CommandSourceStack source) throws CommandSyntaxException;
}
public static Set<ServerComputer> unwrap(CommandSourceStack source, Collection<ComputersSupplier> suppliers) throws CommandSyntaxException {
Set<ServerComputer> computers = new HashSet<>();
for (var supplier : suppliers) computers.addAll(supplier.unwrap(source));
return computers;
}
}

View File

@ -4,6 +4,8 @@
package dan200.computercraft.shared.command.text;
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 static Component link(MutableComponent component, String command, Compone
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 static MutableComponent copy(String text) {
.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.translatable("gui.computercraft.tooltip.copy")))
);
}
public static String makeComputerCommand(String command, ServerComputer computer) {
return String.format("/computercraft %s @c[instance=%s]", command, computer.getInstanceUUID());
}
public static Component makeComputerDumpCommand(ServerComputer computer) {
return link(
text("#" + computer.getID()),
makeComputerCommand("dump", computer),
Component.translatable("commands.computercraft.dump.action")
);
}
}

View File

@ -23,6 +23,7 @@
import net.minecraft.world.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 final void onNeighborChange(BlockState state, LevelReader world, BlockPos
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

View File

@ -36,13 +36,14 @@
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 @@ protected void unload() {
var computer = getServerComputer();
if (computer != null) computer.close();
instanceID = -1;
instanceID = null;
}
@Override
@ -113,19 +114,16 @@ public InteractionResult use(Player player, InteractionHand hand) {
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 @@ protected void serverTick() {
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 @@ protected ComputerSide remapLocalSide(ComputerSide localSide) {
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 @@ private void updateRedstoneInput(ServerComputer computer, Direction dir, BlockPo
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 @@ private void updateInputsImmediately(ServerComputer computer) {
}
}
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 @@ private void updateInputAt(BlockPos neighbour) {
// 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 final ServerComputer createServerComputer() {
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 @@ protected void loadClient(CompoundTag nbt) {
}
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 @@ protected void transferStateFrom(AbstractComputerBlockEntity copy) {
lockCode = copy.lockCode;
BlockEntityHelpers.updateBlock(this);
}
copy.instanceID = -1;
copy.instanceID = null;
}
@Override

View File

@ -51,7 +51,7 @@ public Direction getDirection() {
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);
}
}

View File

@ -26,11 +26,13 @@
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 @@ protected void markTerminalChanged() {
public void tickServer() {
ticksSincePing++;
computer.tick();
changedLastFrame = computer.pollAndResetChanged();
if (terminalChanged.getAndSet(false)) onTerminalChanged();
}
@ -119,13 +117,13 @@ public boolean hasTimedOut() {
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 @@ void unload() {
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 int getInstanceID() {
return instanceID;
}
public UUID getInstanceUUID() {
return instanceUUID;
}
public int getID() {
return computer.getID();
}
@ -167,7 +169,7 @@ public boolean isOn() {
}
public ComputerState getState() {
if (!isOn()) return ComputerState.OFF;
if (!computer.isOn()) return ComputerState.OFF;
return computer.isBlinking() ? ComputerState.BLINKING : ComputerState.ON;
}

View File

@ -8,14 +8,14 @@
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 @@ int getUnusedInstanceID() {
@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 @@ void update() {
}
}
void add(int instanceID, ServerComputer computer) {
remove(instanceID);
computers.put(instanceID, computer);
nextInstanceId = Math.max(nextInstanceId, instanceID + 1);
}
void add(ServerComputer computer) {
var instanceID = computer.getInstanceID();
var instanceUUID = computer.getInstanceUUID();
void remove(int instanceID) {
var computer = get(instanceID);
if (computer != null) {
computer.unload();
computer.onRemoved();
if (computersByInstanceId.containsKey(instanceID)) {
throw new IllegalStateException("Duplicate computer " + instanceID);
}
computers.remove(instanceID);
if (computersByInstanceUuid.containsKey(instanceUUID)) {
throw new IllegalStateException("Duplicate computer " + instanceUUID);
}
computersByInstanceId.put(instanceID, computer);
computersByInstanceUuid.put(instanceUUID, computer);
}
void remove(ServerComputer computer) {
computer.unload();
computer.onRemoved();
computersByInstanceId.remove(computer.getInstanceID());
computersByInstanceUuid.remove(computer.getInstanceUUID());
}
void close() {
for (var computer : getComputers()) computer.unload();
computers.clear();
computersByInstanceId.clear();
computersByInstanceUuid.clear();
}
public Collection<ServerComputer> getComputers() {
return computers.values();
return computersByInstanceId.values();
}
}

View File

@ -25,7 +25,7 @@ public ViewComputerMenu(int id, Inventory player, ComputerContainerData data) {
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;
}

View File

@ -8,6 +8,7 @@
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.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);

View File

@ -13,24 +13,26 @@
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 PocketComputerDataMessage(FriendlyByteBuf buf) {
@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 void write(FriendlyByteBuf buf) {
@Override
public void handle(ClientNetworkContext context) {
context.handlePocketComputerData(instanceId, state, lightState, terminal);
context.handlePocketComputerData(clientId, state, lightState, terminal);
}
@Override

View File

@ -9,21 +9,23 @@
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

View File

@ -7,11 +7,11 @@
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 @@
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 SpeakerAudioClientMessage(FriendlyByteBuf buf) {
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 void write(FriendlyByteBuf buf) {
buf.writeUUID(source);
pos.write(buf);
buf.writeFloat(volume);
buf.writeBytes(content.duplicate());
content.write(buf);
}
@Override

View File

@ -11,9 +11,8 @@
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 String[] getMethodNames() {
@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

View File

@ -10,6 +10,9 @@
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 @@
* 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) {

View File

@ -127,9 +127,8 @@ public boolean onCustomDestroyBlock(BlockState state, Level world, BlockPos pos,
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 ItemStack getCloneItemStack(BlockState state, @Nullable HitResult hit, Le
@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 FluidState getFluidState(BlockState state) {
@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 static BlockState correctConnections(Level world, BlockPos pos, BlockStat
@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;
}

View File

@ -7,7 +7,6 @@
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.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 @@ protected void detachPeripheral(String name) {
}
}
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 CableBlockEntity(BlockEntityType<? extends CableBlockEntity> type, BlockP
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 @@ InteractionResult use(Player player) {
@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 @@ private void updateBlockState() {
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 @@ private void updateBlockState() {
void blockTick() {
if (getLevel().isClientSide) return;
if (invalidPeripheral) refreshPeripheral();
if (refreshPeripheral) {
refreshPeripheral = false;
if (isPeripheralOn()) attachPeripheral();
}
if (modem.getModemState().pollChanged()) updateBlockState();
if (!connectionsFormed) {
connectionsFormed = true;
connectionsChanged();
if (peripheralAccessAllowed) {
peripheral.attach(level, worldPosition, getDirection());
updateConnectedPeripherals();
}
}
if (connectionsChanged) connectionsChanged();
if (refreshConnections) connectionsChanged();
}
private void scheduleConnectionsChanged() {
connectionsChanged = true;
void scheduleConnectionsChanged() {
refreshConnections = true;
TickScheduler.schedule(tickToken);
}
void connectionsChanged() {
if (getLevel().isClientSide) return;
connectionsChanged = false;
refreshConnections = false;
var state = getBlockState();
var world = getLevel();
@ -263,56 +203,29 @@ void connectionsChanged() {
if (CableBlock.canConnectIn(state, facing)) {
// If we can connect to it then do so
this.node.connectTo(node);
} else if (this.node.getNetwork() == node.getNetwork()) {
// Otherwise if we're on the same network then attempt to void it.
} else {
// Otherwise break the connection.
this.node.disconnectFrom(node);
}
}
// If we can no longer attach peripherals, then detach any which may have existed
if (!canAttachPeripheral()) detachPeripheral();
}
void modemChanged() {
// Tell anyone who cares that the connection state has changed
PlatformHelper.get().invalidateComponent(this);
if (getLevel().isClientSide) return;
// If we can no longer attach peripherals, then detach any
// which may have existed
if (!canAttachPeripheral() && peripheralAccessAllowed) {
peripheralAccessAllowed = false;
peripheral.detach();
node.updatePeripherals(Map.of());
setChanged();
updateBlockState();
}
private void attachPeripheral() {
var dir = Objects.requireNonNull(getModemDirection(), "Attaching without a modem");
if (peripheral.attach(getLevel(), getBlockPos(), dir)) updateConnectedPeripherals();
updateBlockState();
}
private void togglePeripheralAccess() {
if (!peripheralAccessAllowed) {
peripheral.attach(level, getBlockPos(), getDirection());
if (!peripheral.hasPeripheral()) return;
peripheralAccessAllowed = true;
node.updatePeripherals(peripheral.toMap());
} else {
peripheral.detach();
peripheralAccessAllowed = false;
node.updatePeripherals(Map.of());
}
private void detachPeripheral() {
if (peripheral.detach()) updateConnectedPeripherals();
updateBlockState();
}
private void updateConnectedPeripherals() {
var peripherals = peripheral.toMap();
if (peripherals.isEmpty()) {
// If there are no peripherals then disable access and update the display state.
peripheralAccessAllowed = false;
updateBlockState();
}
node.updatePeripherals(peripherals);
node.updatePeripherals(peripheral.toMap());
}
@Nullable
@ -322,7 +235,11 @@ public WiredElement getWiredElement(@Nullable Direction direction) {
@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() {

View File

@ -31,15 +31,12 @@ boolean placeAt(Level world, BlockPos pos, BlockState state) {
// 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;
}

View File

@ -10,49 +10,57 @@
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 String getSerializedName() {
return facing;
}
public boolean isModemOn() {
return modemOn;
}
public boolean isPeripheralOn() {
return peripheralOn;
}
@Override
public String toString() {
return name;

View File

@ -7,12 +7,14 @@
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 final InteractionResult use(BlockState state, Level world, BlockPos pos,
@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

View File

@ -32,8 +32,6 @@
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 Vec3 getPosition() {
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 WiredModemFullBlockEntity(BlockEntityType<WiredModemFullBlockEntity> type
@Override
public void setRemoved() {
super.setRemoved();
if (level == null || !level.isClientSide) {
node.remove();
connectionsFormed = false;
for (var modem : modems) {
if (modem != null) modem.removed();
}
if (level == null || !level.isClientSide) node.remove();
}
@Override
public void clearRemoved() {
super.clearRemoved();
refreshConnections = true;
invalidSides = DirectionUtil.ALL_SIDES;
TickScheduler.schedule(tickToken);
}
void neighborChanged(BlockPos neighbour) {
if (!level.isClientSide && peripheralAccessAllowed) {
for (var facing : DirectionUtil.FACINGS) {
if (getBlockPos().relative(facing).equals(neighbour)) queueRefreshPeripheral(facing);
}
for (var facing : DirectionUtil.FACINGS) {
if (getBlockPos().relative(facing).equals(neighbour)) queueRefreshPeripheral(facing);
}
}
private void queueRefreshPeripheral(Direction facing) {
if (invalidSides == 0) TickScheduler.schedule(tickToken);
void queueRefreshPeripheral(Direction facing) {
invalidSides |= 1 << facing.ordinal();
}
private void refreshPeripheral(Direction facing) {
invalidSides &= ~(1 << facing.ordinal());
var peripheral = peripherals[facing.ordinal()];
if (level != null && !isRemoved() && peripheral.attach(level, getBlockPos(), facing)) {
updateConnectedPeripherals();
}
TickScheduler.schedule(tickToken);
}
public InteractionResult use(Player player) {
@ -129,7 +124,11 @@ public InteractionResult use(Player player) {
// 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 @@ private static void sendPeripheralChanges(Player player, String kind, Collection
@Override
public void load(CompoundTag nbt) {
super.load(nbt);
peripheralAccessAllowed = nbt.getBoolean(NBT_PERIPHERAL_ENABLED);
for (var i = 0; i < peripherals.length; i++) peripherals[i].read(nbt, Integer.toString(i));
}
@Override
public void saveAdditional(CompoundTag nbt) {
nbt.putBoolean(NBT_PERIPHERAL_ENABLED, peripheralAccessAllowed);
for (var i = 0; i < peripherals.length; i++) peripherals[i].write(nbt, Integer.toString(i));
super.saveAdditional(nbt);
}
private void updateBlockState() {
var state = getBlockState();
boolean modemOn = modemState.isOpen(), peripheralOn = peripheralAccessAllowed;
if (state.getValue(MODEM_ON) == modemOn && state.getValue(PERIPHERAL_ON) == peripheralOn) return;
getLevel().setBlockAndUpdate(getBlockPos(), state.setValue(MODEM_ON, modemOn).setValue(PERIPHERAL_ON, peripheralOn));
}
@Override
public void clearRemoved() {
super.clearRemoved();
TickScheduler.schedule(tickToken);
}
void blockTick() {
if (getLevel().isClientSide) return;
if (invalidSides != 0) {
for (var direction : DirectionUtil.FACINGS) {
if ((invalidSides & (1 << direction.ordinal())) != 0) refreshPeripheral(direction);
}
var oldInvalidSides = invalidSides;
invalidSides = 0;
if (isPeripheralOn()) attachPeripherals(oldInvalidSides);
}
if (modemState.pollChanged()) updateBlockState();
if (modemState.pollChanged()) updateModemBlockState();
if (!connectionsFormed) {
connectionsFormed = true;
if (refreshConnections) connectionsChanged();
}
connectionsChanged();
if (peripheralAccessAllowed) {
for (var facing : DirectionUtil.FACINGS) {
peripherals[facing.ordinal()].attach(level, getBlockPos(), facing);
}
updateConnectedPeripherals();
}
}
private void updateModemBlockState() {
var state = getBlockState();
var modemOn = modemState.isOpen();
if (state.getValue(MODEM_ON) == modemOn) return;
if (connectionsChanged) connectionsChanged();
getLevel().setBlockAndUpdate(getBlockPos(), state.setValue(MODEM_ON, modemOn));
}
private void scheduleConnectionsChanged() {
connectionsChanged = true;
refreshConnections = true;
TickScheduler.schedule(tickToken);
}
private void connectionsChanged() {
if (getLevel().isClientSide) return;
connectionsChanged = false;
refreshConnections = false;
var world = getLevel();
var current = getBlockPos();
@ -231,57 +210,48 @@ private void connectionsChanged() {
}
}
private void togglePeripheralAccess() {
if (!peripheralAccessAllowed) {
var hasAny = false;
for (var facing : DirectionUtil.FACINGS) {
var peripheral = peripherals[facing.ordinal()];
peripheral.attach(level, getBlockPos(), facing);
hasAny |= peripheral.hasPeripheral();
}
if (!hasAny) return;
peripheralAccessAllowed = true;
node.updatePeripherals(getConnectedPeripherals());
} else {
peripheralAccessAllowed = false;
for (var peripheral : peripherals) peripheral.detach();
node.updatePeripherals(Map.of());
}
updateBlockState();
}
private Set<String> getConnectedPeripheralNames() {
if (!peripheralAccessAllowed) return Set.of();
Set<String> peripherals = new HashSet<>(6);
private List<String> getConnectedPeripheralNames() {
List<String> peripherals = new ArrayList<>(6);
for (var peripheral : this.peripherals) {
var name = peripheral.getConnectedName();
if (name != null) peripherals.add(name);
}
peripherals.sort(String::compareTo);
return peripherals;
}
private Map<String, IPeripheral> getConnectedPeripherals() {
if (!peripheralAccessAllowed) return Map.of();
private void attachPeripherals(int sides) {
var anyChanged = false;
Map<String, IPeripheral> peripherals = new HashMap<>(6);
for (var peripheral : this.peripherals) peripheral.extendMap(peripherals);
return Collections.unmodifiableMap(peripherals);
}
Map<String, IPeripheral> attachedPeripherals = new HashMap<>(6);
private void updateConnectedPeripherals() {
var peripherals = getConnectedPeripherals();
if (peripherals.isEmpty()) {
// If there are no peripherals then disable access and update the display state.
peripheralAccessAllowed = false;
updateBlockState();
for (var facing : DirectionUtil.FACINGS) {
var peripheral = peripherals[facing.ordinal()];
if (DirectionUtil.isSet(sides, facing)) anyChanged |= peripheral.attach(getLevel(), getBlockPos(), facing);
peripheral.extendMap(attachedPeripherals);
}
node.updatePeripherals(peripherals);
if (anyChanged) node.updatePeripherals(attachedPeripherals);
updatePeripheralBlocKState(!attachedPeripherals.isEmpty());
}
private void detachPeripherals() {
var anyChanged = false;
for (var peripheral : peripherals) anyChanged |= peripheral.detach();
if (anyChanged) node.updatePeripherals(Map.of());
updatePeripheralBlocKState(false);
}
private void updatePeripheralBlocKState(boolean peripheralOn) {
var state = getBlockState();
if (state.getValue(PERIPHERAL_ON) == peripheralOn) return;
getLevel().setBlockAndUpdate(getBlockPos(), state.setValue(PERIPHERAL_ON, peripheralOn));
}
private boolean isPeripheralOn() {
return getBlockState().getValue(PERIPHERAL_ON);
}
public WiredElement getElement() {
@ -295,22 +265,11 @@ public WiredModemPeripheral getPeripheral(@Nullable Direction side) {
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;
}
};
}
}

View File

@ -6,6 +6,7 @@
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.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 boolean attach(Level world, BlockPos origin, Direction direction) {
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 String getConnectedName() {
return peripheral != null ? type + "_" + id : null;
}
@Nullable
public IPeripheral getPeripheral() {
return peripheral;
}
public boolean hasPeripheral() {
return peripheral != null;
}
@ -100,9 +95,7 @@ public void extendMap(Map<String, IPeripheral> peripherals) {
}
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) {

View File

@ -15,6 +15,7 @@
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.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 @@ protected PacketNetwork getNetwork() {
public Level getLevel() {
return modem.getLevel();
}
protected abstract WiredModemLocalPeripheral getLocalPeripheral();
//endregion
@Override
@ -207,7 +216,7 @@ public final MethodResult callRemote(IComputerAccess computer, ILuaContext conte
*/
@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 void attach(IComputerAccess computer) {
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 void detach(IComputerAccess computer) {
}
@Override
public boolean equals(@Nullable IPeripheral other) {
if (other instanceof WiredModemPeripheral otherModem) {
return otherModem.modem == modem;
}
return false;
public final boolean equals(@Nullable IPeripheral other) {
return other instanceof WiredModemPeripheral otherModem && otherModem.modem == modem;
}
@Override
public final Object getTarget() {
return target;
}
//endregion
@ -272,12 +282,11 @@ public void detachPeripheral(String name) {
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 @@ private void attachPeripheralImpl(IComputerAccess computer, ConcurrentMap<String
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 @@ private static class RemotePeripheralWrapper implements IComputerAccess {
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 Collection<String> getMethodNames() {
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

View File

@ -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 @@ void destroy() {
@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 ServerMonitor getCachedServerMonitor() {
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 @@ private void createServerTerminal() {
}
@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 final CompoundTag getUpdateTag() {
}
private void onClientLoad(int oldXIndex, int oldYIndex) {
if (oldXIndex != xIndex || oldYIndex != yIndex) {
// If our index has changed then it's possible the origin monitor has changed. Thus
// we'll clear our cache. If we're the origin then we'll need to remove the glList as well.
if (oldXIndex == 0 && oldYIndex == 0 && clientMonitor != null) clientMonitor.destroy();
if ((oldXIndex != xIndex || oldYIndex != yIndex) && clientMonitor != null) {
// If our index has changed, and we were the origin, then destroy the current monitor.
clientMonitor.destroy();
clientMonitor = null;
}
if (xIndex == 0 && yIndex == 0) {
// If we're the origin terminal then create it.
if (clientMonitor == null) clientMonitor = new ClientMonitor(this);
}
// If we're the origin terminal then create it.
if (xIndex == 0 && yIndex == 0 && clientMonitor == null) clientMonitor = new ClientMonitor(this);
}
public final void read(TerminalState state) {
@ -286,7 +286,7 @@ public int getYIndex() {
}
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 @@ private MonitorState getLoadedMonitor(int x, int y) {
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 @@ void updateNeighborsDeferred() {
}
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 @@ private boolean checkInvariants() {
}
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++) {

View File

@ -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 @@ synchronized boolean pushBuffer(LuaTable<?, ?> table, int size, Optional<Double>
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 @@ synchronized boolean pushBuffer(LuaTable<?, ?> table, int size, Optional<Double>
buffer.flip();
pendingAudio = buffer;
pendingAudio = new EncodedAudio(initialCharge, initialStrength, initialPreviousBit, buffer);
pendingVolume = (float) clampVolume(volume.orElse((double) pendingVolume));
return true;
}
@ -89,12 +93,12 @@ boolean shouldSendPending(long now) {
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;
}

View File

@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.peripheral.speaker;
import net.minecraft.network.FriendlyByteBuf;
import java.nio.ByteBuffer;
/**
* A chunk of encoded audio, along with the state required for the decoder to reproduce the original audio samples.
*
* @param charge The DFPWM charge.
* @param strength The DFPWM strength.
* @param previousBit The previous bit.
* @param audio The block of encoded audio.
*/
public record EncodedAudio(int charge, int strength, boolean previousBit, ByteBuffer audio) {
public void write(FriendlyByteBuf buf) {
buf.writeVarInt(charge());
buf.writeVarInt(strength());
buf.writeBoolean(previousBit());
buf.writeBytes(audio().duplicate());
}
public static EncodedAudio read(FriendlyByteBuf buf) {
var charge = buf.readVarInt();
var strength = buf.readVarInt();
var previousBit = buf.readBoolean();
var bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
return new EncodedAudio(charge, strength, previousBit, ByteBuffer.wrap(bytes));
}
}

View File

@ -375,4 +375,13 @@ default double getReachDistance(Player player) {
* @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;
}
}

View File

@ -10,6 +10,7 @@
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 int getLight() {
@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 void tickServer() {
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 @@ protected void onTerminalChanged() {
@Override
protected void onRemoved() {
super.onRemoved();
ServerNetworking.sendToAllPlayers(new PocketComputerDeletedClientMessage(getInstanceID()), getLevel().getServer());
ServerNetworking.sendToAllPlayers(new PocketComputerDeletedClientMessage(getInstanceUUID()), getLevel().getServer());
}
}

View File

@ -43,6 +43,7 @@
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 String getCreatorModId(ItemStack stack) {
}
public PocketServerComputer createServerComputer(ServerLevel level, Entity entity, @Nullable Container inventory, ItemStack stack) {
var sessionID = getSessionID(stack);
var registry = ServerContext.get(level.getServer()).registry();
var 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 PocketServerComputer createServerComputer(ServerLevel level, Entity entit
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 boolean setLabel(ItemStack stack, @Nullable String label) {
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 @@ private static int getSessionID(ItemStack stack) {
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);

View File

@ -25,10 +25,9 @@
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 void loadServer(CompoundTag nbt) {
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 void loadServer(CompoundTag nbt) {
@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 void setDirection(Direction dir) {
if (dir.getAxis() == Direction.Axis.Y) dir = Direction.NORTH;
level.setBlockAndUpdate(worldPosition, getBlockState().setValue(TurtleBlock.FACING, dir));
updateOutput();
updateRedstone();
updateInputsImmediately();
onTileEntityChange();

View File

@ -14,6 +14,7 @@
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 boolean teleportTo(Level world, BlockPos pos) {
oldWorld.removeBlock(oldPos, false);
// Make sure everybody knows about it
newTurtle.updateOutput();
newTurtle.updateRedstone();
newTurtle.updateInputsImmediately();
return true;
}
@ -589,7 +590,7 @@ private void updatePeripherals(ServerComputer serverComputer) {
}
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 {

View File

@ -54,7 +54,7 @@ public TurtleCommandResult execute(ITurtleAccess turtle) {
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();

View File

@ -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 static float toPitchAngle(Direction dir) {
default -> 0.0f;
};
}
/**
* Determine if a direction is in a bitmask.
*
* @param mask The bitmask to test
* @param direction The direction to check.
* @return Whether the direction is in a bitmask.
*/
public static boolean isSet(int mask, Direction direction) {
return (mask & (1 << direction.ordinal())) != 0;
}
}

View File

@ -5,13 +5,20 @@
package dan200.computercraft.shared.util;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ChunkLevel;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.chunk.ChunkStatus;
import net.minecraft.world.level.chunk.LevelChunk;
import java.util.Queue;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* A thread-safe version of {@link LevelAccessor#scheduleTick(BlockPos, Block, int)}.
@ -22,26 +29,92 @@ public final class TickScheduler {
private TickScheduler() {
}
/**
* The list of block entities to tick.
*/
private static final Queue<Token> toTick = new ConcurrentLinkedDeque<>();
/**
* Block entities which we want to tick, but whose chunks not currently loaded.
* <p>
* Minecraft sometimes keeps chunks in-memory, but not actively loaded. If such a block entity is in the
* {@link #toTick} queue, we'll see that it's not loaded and so have to skip scheduling a tick.
* <p>
* However, if the block entity is ever loaded again, we need to tick it. Unfortunately, block entities in this
* state are not notified in any way (for instance, {@link BlockEntity#setRemoved()} or
* {@link BlockEntity#clearRemoved()} are not called), and so there's no way to easily reschedule them for ticking.
* <p>
* Instead, for each chunk we keep a list of all block entities whose tick we skipped. If a chunk is loaded,
* {@linkplain #onChunkTicketChanged(ServerLevel, long, int, int) we requeue all skipped ticks}.
*/
private static final Map<ChunkReference, List<Token>> delayed = new HashMap<>();
/**
* Schedule a block entity to be ticked.
*
* @param token The token whose block entity should be ticked.
*/
public static void schedule(Token token) {
var world = token.owner.getLevel();
if (world != null && !world.isClientSide && !token.scheduled.getAndSet(true)) toTick.add(token);
if (world != null && !world.isClientSide && Token.STATE.compareAndSet(token, State.IDLE, State.SCHEDULED)) {
toTick.add(token);
}
}
public static void onChunkTicketChanged(ServerLevel level, long chunkPos, int oldLevel, int newLevel) {
boolean oldLoaded = isLoaded(oldLevel), newLoaded = isLoaded(newLevel);
if (!oldLoaded && newLoaded) {
// If our chunk is becoming active, requeue all pending tokens.
var delayedTokens = delayed.remove(new ChunkReference(level.dimension(), chunkPos));
if (delayedTokens == null) return;
for (var token : delayedTokens) {
if (token.owner.isRemoved()) {
Token.STATE.set(token, State.IDLE);
} else {
Token.STATE.set(token, State.SCHEDULED);
toTick.add(token);
}
}
}
}
public static void onChunkUnload(LevelChunk chunk) {
// If our chunk is fully unloaded, all block entities are about to be removed - we need to dequeue any delayed
// tokens from the queue.
var delayedTokens = delayed.remove(new ChunkReference(chunk.getLevel().dimension(), chunk.getPos().toLong()));
if (delayedTokens == null) return;
for (var token : delayedTokens) Token.STATE.set(token, State.IDLE);
}
public static void tick() {
Token token;
while ((token = toTick.poll()) != null) {
token.scheduled.set(false);
var blockEntity = token.owner;
if (blockEntity.isRemoved()) continue;
while ((token = toTick.poll()) != null) Token.STATE.set(token, tickToken(token));
}
var world = blockEntity.getLevel();
var pos = blockEntity.getBlockPos();
private static State tickToken(Token token) {
var blockEntity = token.owner;
if (world != null && world.isLoaded(pos) && world.getBlockEntity(pos) == blockEntity) {
world.scheduleTick(pos, blockEntity.getBlockState().getBlock(), 0);
// If the block entity has been removed, then remove it from the queue.
if (blockEntity.isRemoved()) return State.IDLE;
var level = Objects.requireNonNull(blockEntity.getLevel(), "Block entity level cannot become null");
var pos = blockEntity.getBlockPos();
if (!level.isLoaded(pos)) {
// The chunk is not properly loaded, as it to our delayed set.
delayed.computeIfAbsent(new ChunkReference(level.dimension(), ChunkPos.asLong(pos)), x -> new ArrayList<>()).add(token);
return State.UNLOADED;
} else {
// This should be impossible: either the block entity is at the above position, or it has been removed.
if (level.getBlockEntity(pos) != blockEntity) {
throw new IllegalStateException("Expected " + blockEntity + " at " + pos);
}
// Otherwise schedule a tick and remove it from the queue.
level.scheduleTick(pos, blockEntity.getBlockState().getBlock(), 0);
return State.IDLE;
}
}
@ -52,11 +125,51 @@ public static void tick() {
* 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);
}
}

View File

@ -7,7 +7,6 @@ accessWidener v1 named
# Additional access wideners for vanilla code. This is a effectively the subset of Fabric's transitive access wideners
# 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;

View File

@ -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 void testDecodesBytes() {
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");

View File

@ -0,0 +1,183 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.impl.network.wired;
import dan200.computercraft.api.network.wired.WiredNetwork;
import dan200.computercraft.impl.network.wired.NetworkTest.NetworkElement;
import dan200.computercraft.shared.util.DirectionUtil;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import net.minecraft.core.BlockPos;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class NetworkBenchmark {
private static final int BRUTE_SIZE = 16;
public static void main(String[] args) throws RunnerException {
var opts = new OptionsBuilder()
.include(NetworkBenchmark.class.getName() + "\\..*")
.warmupIterations(2)
.measurementIterations(5)
.forks(1)
.build();
new Runner(opts).run();
}
@Benchmark
@Warmup(time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(time = 2, timeUnit = TimeUnit.SECONDS)
public void removeEveryNode(ConnectedGrid grid) {
grid.grid.forEach((node, pos) -> node.remove());
}
@Benchmark
@Warmup(time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(time = 2, timeUnit = TimeUnit.SECONDS)
public void connectAndDisconnect(SplitGrid connectedGrid) {
WiredNodeImpl left = connectedGrid.left, right = connectedGrid.right;
assertNotEquals(left.getNetwork(), right.getNetwork());
left.connectTo(right);
assertEquals(left.getNetwork(), right.getNetwork());
left.disconnectFrom(right);
assertNotEquals(left.getNetwork(), right.getNetwork());
}
@Benchmark
@Warmup(time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(time = 2, timeUnit = TimeUnit.SECONDS)
public void connectAndRemove(SplitGrid connectedGrid) {
WiredNodeImpl left = connectedGrid.left, right = connectedGrid.right, centre = connectedGrid.centre;
assertNotEquals(left.getNetwork(), right.getNetwork());
centre.connectTo(left);
centre.connectTo(right);
assertEquals(left.getNetwork(), right.getNetwork());
centre.remove();
assertNotEquals(left.getNetwork(), right.getNetwork());
}
/**
* Create a grid where all nodes are connected to their neighbours.
*/
@State(Scope.Thread)
public static class ConnectedGrid {
Grid<WiredNodeImpl> grid;
@Setup(Level.Invocation)
public void setup() {
var grid = this.grid = new Grid<>(BRUTE_SIZE);
grid.map((existing, pos) -> new NetworkElement("n_" + pos, pos.getX() == pos.getY() && pos.getY() == pos.getZ()).getNode());
// Connect every node
grid.forEach((node, pos) -> {
for (var facing : DirectionUtil.FACINGS) {
var other = grid.get(pos.relative(facing));
if (other != null) node.connectTo(other);
}
});
var networks = countNetworks(grid);
if (networks.size() != 1) throw new AssertionError("Expected exactly one network.");
}
}
/**
* Create a grid where the nodes at {@code x < BRUTE_SIZE/2} and {@code x >= BRUTE_SIZE/2} are in separate networks,
* but otherwise connected to their neighbours.
*/
@State(Scope.Thread)
public static class SplitGrid {
Grid<WiredNodeImpl> grid;
WiredNodeImpl left, right, centre;
@Setup
public void setup() {
var grid = this.grid = new Grid<>(BRUTE_SIZE);
grid.map((existing, pos) -> new NetworkElement("n_" + pos, pos.getX() == pos.getY() && pos.getY() == pos.getZ()).getNode());
// Connect every node
grid.forEach((node, pos) -> {
for (var facing : DirectionUtil.FACINGS) {
var offset = pos.relative(facing);
if (offset.getX() >= BRUTE_SIZE / 2 == pos.getX() >= BRUTE_SIZE / 2) {
var other = grid.get(offset);
if (other != null) node.connectTo(other);
}
}
});
var networks = countNetworks(grid);
if (networks.size() != 2) throw new AssertionError("Expected exactly two networks.");
for (var network : networks.object2IntEntrySet()) {
if (network.getIntValue() != BRUTE_SIZE * BRUTE_SIZE * (BRUTE_SIZE / 2)) {
throw new AssertionError("Network is the wrong size");
}
}
left = Objects.requireNonNull(grid.get(new BlockPos(BRUTE_SIZE / 2 - 1, 0, 0)));
right = Objects.requireNonNull(grid.get(new BlockPos(BRUTE_SIZE / 2, 0, 0)));
centre = new NetworkElement("c", false).getNode();
}
}
private static Object2IntMap<WiredNetwork> countNetworks(Grid<WiredNodeImpl> grid) {
Object2IntMap<WiredNetwork> networks = new Object2IntOpenHashMap<>();
grid.forEach((node, pos) -> networks.put(node.network, networks.getOrDefault(node.network, 0) + 1));
return networks;
}
private static class Grid<T> {
private final int size;
private final T[] box;
@SuppressWarnings("unchecked")
Grid(int size) {
this.size = size;
this.box = (T[]) new Object[size * size * size];
}
public T get(BlockPos pos) {
int x = pos.getX(), y = pos.getY(), z = pos.getZ();
return x >= 0 && x < size && y >= 0 && y < size && z >= 0 && z < size
? box[x * size * size + y * size + z]
: null;
}
public void forEach(BiConsumer<T, BlockPos> transform) {
for (var x = 0; x < size; x++) {
for (var y = 0; y < size; y++) {
for (var z = 0; z < size; z++) {
transform.accept(box[x * size * size + y * size + z], new BlockPos(x, y, z));
}
}
}
}
public void map(BiFunction<T, BlockPos, T> transform) {
for (var x = 0; x < size; x++) {
for (var y = 0; y < size; y++) {
for (var z = 0; z < size; z++) {
box[x * size * size + y * size + z] = transform.apply(box[x * size * size + y * size + z], new BlockPos(x, y, z));
}
}
}
}
}
}

View File

@ -9,19 +9,14 @@
import dan200.computercraft.api.network.wired.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 void testConnect() {
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 void testConnect() {
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 void testConnect() {
@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 void testDisconnectNoChange() {
@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 void testDisconnectLeaf() {
@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 void testDisconnectSplit() {
@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 void testRemoveSingle() {
@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 void testRemoveLeaf() {
@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 void testRemoveSplit() {
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 String toString() {
}
@Override
public WiredNode getNode() {
public WiredNodeImpl getNode() {
return node;
}
@ -364,45 +293,6 @@ public boolean equals(@Nullable IPeripheral other) {
}
}
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;
}

View File

@ -0,0 +1,91 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.command.arguments;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.context.StringRange;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestion;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import dan200.computercraft.shared.command.Exceptions;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.test.core.ReplaceUnderscoresDisplayNameGenerator;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.List;
import java.util.Map;
import java.util.OptionalInt;
import java.util.UUID;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@DisplayNameGeneration(ReplaceUnderscoresDisplayNameGenerator.class)
class ComputerSelectorTest {
@ParameterizedTest(name = "{0}")
@MethodSource("getArgumentTestCases")
public void Parse_basic_inputs(String input, ComputerSelector expected) throws CommandSyntaxException {
assertEquals(expected, ComputerSelector.parse(new StringReader(input)));
}
public static Arguments[] getArgumentTestCases() {
return new Arguments[]{
// Legacy selectors
Arguments.of("@some_label", new ComputerSelector("@some_label", OptionalInt.empty(), null, OptionalInt.empty(), "some_label", null, null, null)),
Arguments.of("~normal", new ComputerSelector("~normal", OptionalInt.empty(), null, OptionalInt.empty(), null, ComputerFamily.NORMAL, null, null)),
Arguments.of("#123", new ComputerSelector("#123", OptionalInt.empty(), null, OptionalInt.of(123), null, null, null, null)),
Arguments.of("123", new ComputerSelector("123", OptionalInt.of(123), null, OptionalInt.empty(), null, null, null, null)),
// New selectors
Arguments.of("@c[]", new ComputerSelector("@c[]", OptionalInt.empty(), null, OptionalInt.empty(), null, null, null, null)),
Arguments.of("@c[instance=5e18f505-62f7-46f8-83f3-792f03224724]", new ComputerSelector("@c[instance=5e18f505-62f7-46f8-83f3-792f03224724]", OptionalInt.empty(), UUID.fromString("5e18f505-62f7-46f8-83f3-792f03224724"), OptionalInt.empty(), null, null, null, null)),
Arguments.of("@c[id=123]", new ComputerSelector("@c[id=123]", OptionalInt.empty(), null, OptionalInt.of(123), null, null, null, null)),
Arguments.of("@c[label=\"foo\"]", new ComputerSelector("@c[label=\"foo\"]", OptionalInt.empty(), null, OptionalInt.empty(), "foo", null, null, null)),
Arguments.of("@c[family=normal]", new ComputerSelector("@c[family=normal]", OptionalInt.empty(), null, OptionalInt.empty(), null, ComputerFamily.NORMAL, null, null)),
// Complex selectors
Arguments.of("@c[ id = 123 , ]", new ComputerSelector("@c[ id = 123 , ]", OptionalInt.empty(), null, OptionalInt.of(123), null, null, null, null)),
Arguments.of("@c[id=123,family=normal]", new ComputerSelector("@c[id=123,family=normal]", OptionalInt.empty(), null, OptionalInt.of(123), null, ComputerFamily.NORMAL, null, null)),
};
}
@Test
public void Fails_on_repeated_options() {
var error = assertThrows(CommandSyntaxException.class, () -> ComputerSelector.parse(new StringReader("@c[id=1, id=2]")));
assertEquals(Exceptions.ERROR_INAPPLICABLE_OPTION, error.getType());
}
@Test
public void Complete_selector_components() {
assertEquals(List.of(new Suggestion(StringRange.between(0, 1), "@c[")), suggest("@"));
assertThat(suggest("@c["), hasItem(
new Suggestion(StringRange.at(3), "family", ComputerSelector.options().get("family").tooltip())
));
assertEquals(List.of(new Suggestion(StringRange.at(9), "=")), suggest("@c[family"));
assertEquals(List.of(new Suggestion(StringRange.at(16), ",")), suggest("@c[family=normal"));
}
@Test
public void Complete_selector_family() {
assertThat(suggest("@c[family="), containsInAnyOrder(
new Suggestion(StringRange.at(10), "normal"),
new Suggestion(StringRange.at(10), "advanced"),
new Suggestion(StringRange.at(10), "command")
));
assertThat(suggest("@c[family=n"), contains(
new Suggestion(StringRange.between(10, 11), "normal")
));
}
private List<Suggestion> suggest(String input) {
var context = new CommandContext<>(new Object(), "", Map.of(), null, null, List.of(), StringRange.at(0), null, null, false);
return ComputerSelector.suggest(context, new SuggestionsBuilder(input, 0)).getNow(null).getList();
}
}

View File

@ -23,7 +23,7 @@ public void testEncoder() throws LuaException {
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);

View File

@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.peripheral.speaker;
import dan200.computercraft.test.core.ArbitraryByteBuffer;
import io.netty.buffer.Unpooled;
import net.jqwik.api.*;
import net.minecraft.network.FriendlyByteBuf;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
class EncodedAudioTest {
/**
* Sends the audio on a roundtrip, ensuring that its contents are reassembled on the other end.
*
* @param audio The message to send.
*/
@Property
public void testRoundTrip(@ForAll("audio") EncodedAudio audio) {
var buffer = new FriendlyByteBuf(Unpooled.directBuffer());
audio.write(buffer);
var converted = EncodedAudio.read(buffer);
assertEquals(buffer.readableBytes(), 0, "Whole packet was read");
assertThat("Messages are equal", converted, equalTo(converted));
}
@Provide
Arbitrary<EncodedAudio> audio() {
return Combinators.combine(
Arbitraries.integers(),
Arbitraries.integers(),
Arbitraries.of(true, false),
ArbitraryByteBuffer.bytes().ofMaxSize(1000)
).as(EncodedAudio::new);
}
}

View File

@ -11,11 +11,15 @@
import dan200.computercraft.core.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 @@ fun Computer_peripheral(context: GameTestHelper) = context.sequence {
}
}
/**
* 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.
*/

View File

@ -7,18 +7,21 @@
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 @@ fun Full_modems_form_networks(helper: GameTestHelper) = helper.sequence {
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 @@ fun Full_modems_form_networks(helper: GameTestHelper) = helper.sequence {
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 @@ fun Full_modems_form_networks(helper: GameTestHelper) = helper.sequence {
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")
}

View File

@ -38,7 +38,7 @@ fun Sync_state(context: GameTestHelper) = context.sequence {
// 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 @@ fun Sync_state(context: GameTestHelper) = context.sequence {
// 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

View File

@ -329,8 +329,6 @@ fun Use_compostors(helper: GameTestHelper) = helper.sequence {
/**
* 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 @@ fun Peripheral_change(helper: GameTestHelper) = helper.sequence {
}
}
// 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.

View File

@ -20,15 +20,19 @@
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 @@ private fun getName(type: BlockEntityType<*>): ResourceLocation =
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))
}

View File

@ -0,0 +1,138 @@
{
DataVersion: 3465,
size: [5, 5, 5],
data: [
{pos: [0, 0, 0], state: "minecraft:polished_andesite"},
{pos: [0, 0, 1], state: "minecraft:polished_andesite"},
{pos: [0, 0, 2], state: "minecraft:polished_andesite"},
{pos: [0, 0, 3], state: "minecraft:polished_andesite"},
{pos: [0, 0, 4], state: "minecraft:polished_andesite"},
{pos: [1, 0, 0], state: "minecraft:polished_andesite"},
{pos: [1, 0, 1], state: "minecraft:polished_andesite"},
{pos: [1, 0, 2], state: "minecraft:polished_andesite"},
{pos: [1, 0, 3], state: "minecraft:polished_andesite"},
{pos: [1, 0, 4], state: "minecraft:polished_andesite"},
{pos: [2, 0, 0], state: "minecraft:polished_andesite"},
{pos: [2, 0, 1], state: "minecraft:polished_andesite"},
{pos: [2, 0, 2], state: "minecraft:polished_andesite"},
{pos: [2, 0, 3], state: "minecraft:polished_andesite"},
{pos: [2, 0, 4], state: "minecraft:polished_andesite"},
{pos: [3, 0, 0], state: "minecraft:polished_andesite"},
{pos: [3, 0, 1], state: "minecraft:polished_andesite"},
{pos: [3, 0, 2], state: "minecraft:polished_andesite"},
{pos: [3, 0, 3], state: "minecraft:polished_andesite"},
{pos: [3, 0, 4], state: "minecraft:polished_andesite"},
{pos: [4, 0, 0], state: "minecraft:polished_andesite"},
{pos: [4, 0, 1], state: "minecraft:polished_andesite"},
{pos: [4, 0, 2], state: "minecraft:polished_andesite"},
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
{pos: [0, 1, 0], state: "minecraft:air"},
{pos: [0, 1, 1], state: "minecraft:air"},
{pos: [0, 1, 2], state: "minecraft:air"},
{pos: [0, 1, 3], state: "minecraft:air"},
{pos: [0, 1, 4], state: "minecraft:air"},
{pos: [1, 1, 0], state: "minecraft:air"},
{pos: [1, 1, 1], state: "minecraft:air"},
{pos: [1, 1, 2], state: "minecraft:air"},
{pos: [1, 1, 3], state: "minecraft:air"},
{pos: [1, 1, 4], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"},
{pos: [2, 1, 1], state: "minecraft:air"},
{pos: [2, 1, 2], state: "minecraft:chest{facing:north,type:single,waterlogged:false}", nbt: {Items: [], id: "minecraft:chest"}},
{pos: [2, 1, 3], state: "minecraft:air"},
{pos: [2, 1, 4], state: "minecraft:air"},
{pos: [3, 1, 0], state: "minecraft:air"},
{pos: [3, 1, 1], state: "minecraft:air"},
{pos: [3, 1, 2], state: "computercraft:computer_normal{facing:north,state:on}", nbt: {ComputerId: 1, Label: "computer_test.chest_resizes_on_change", On: 1b, id: "computercraft:computer_normal"}},
{pos: [3, 1, 3], state: "minecraft:air"},
{pos: [3, 1, 4], state: "minecraft:air"},
{pos: [4, 1, 0], state: "minecraft:air"},
{pos: [4, 1, 1], state: "minecraft:air"},
{pos: [4, 1, 2], state: "minecraft:air"},
{pos: [4, 1, 3], state: "minecraft:air"},
{pos: [4, 1, 4], state: "minecraft:air"},
{pos: [0, 2, 0], state: "minecraft:air"},
{pos: [0, 2, 1], state: "minecraft:air"},
{pos: [0, 2, 2], state: "minecraft:air"},
{pos: [0, 2, 3], state: "minecraft:air"},
{pos: [0, 2, 4], state: "minecraft:air"},
{pos: [1, 2, 0], state: "minecraft:air"},
{pos: [1, 2, 1], state: "minecraft:air"},
{pos: [1, 2, 2], state: "minecraft:air"},
{pos: [1, 2, 3], state: "minecraft:air"},
{pos: [1, 2, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:air"},
{pos: [2, 2, 2], state: "minecraft:air"},
{pos: [2, 2, 3], state: "minecraft:air"},
{pos: [2, 2, 4], state: "minecraft:air"},
{pos: [3, 2, 0], state: "minecraft:air"},
{pos: [3, 2, 1], state: "minecraft:air"},
{pos: [3, 2, 2], state: "minecraft:air"},
{pos: [3, 2, 3], state: "minecraft:air"},
{pos: [3, 2, 4], state: "minecraft:air"},
{pos: [4, 2, 0], state: "minecraft:air"},
{pos: [4, 2, 1], state: "minecraft:air"},
{pos: [4, 2, 2], state: "minecraft:air"},
{pos: [4, 2, 3], state: "minecraft:air"},
{pos: [4, 2, 4], state: "minecraft:air"},
{pos: [0, 3, 0], state: "minecraft:air"},
{pos: [0, 3, 1], state: "minecraft:air"},
{pos: [0, 3, 2], state: "minecraft:air"},
{pos: [0, 3, 3], state: "minecraft:air"},
{pos: [0, 3, 4], state: "minecraft:air"},
{pos: [1, 3, 0], state: "minecraft:air"},
{pos: [1, 3, 1], state: "minecraft:air"},
{pos: [1, 3, 2], state: "minecraft:air"},
{pos: [1, 3, 3], state: "minecraft:air"},
{pos: [1, 3, 4], state: "minecraft:air"},
{pos: [2, 3, 0], state: "minecraft:air"},
{pos: [2, 3, 1], state: "minecraft:air"},
{pos: [2, 3, 2], state: "minecraft:air"},
{pos: [2, 3, 3], state: "minecraft:air"},
{pos: [2, 3, 4], state: "minecraft:air"},
{pos: [3, 3, 0], state: "minecraft:air"},
{pos: [3, 3, 1], state: "minecraft:air"},
{pos: [3, 3, 2], state: "minecraft:air"},
{pos: [3, 3, 3], state: "minecraft:air"},
{pos: [3, 3, 4], state: "minecraft:air"},
{pos: [4, 3, 0], state: "minecraft:air"},
{pos: [4, 3, 1], state: "minecraft:air"},
{pos: [4, 3, 2], state: "minecraft:air"},
{pos: [4, 3, 3], state: "minecraft:air"},
{pos: [4, 3, 4], state: "minecraft:air"},
{pos: [0, 4, 0], state: "minecraft:air"},
{pos: [0, 4, 1], state: "minecraft:air"},
{pos: [0, 4, 2], state: "minecraft:air"},
{pos: [0, 4, 3], state: "minecraft:air"},
{pos: [0, 4, 4], state: "minecraft:air"},
{pos: [1, 4, 0], state: "minecraft:air"},
{pos: [1, 4, 1], state: "minecraft:air"},
{pos: [1, 4, 2], state: "minecraft:air"},
{pos: [1, 4, 3], state: "minecraft:air"},
{pos: [1, 4, 4], state: "minecraft:air"},
{pos: [2, 4, 0], state: "minecraft:air"},
{pos: [2, 4, 1], state: "minecraft:air"},
{pos: [2, 4, 2], state: "minecraft:air"},
{pos: [2, 4, 3], state: "minecraft:air"},
{pos: [2, 4, 4], state: "minecraft:air"},
{pos: [3, 4, 0], state: "minecraft:air"},
{pos: [3, 4, 1], state: "minecraft:air"},
{pos: [3, 4, 2], state: "minecraft:air"},
{pos: [3, 4, 3], state: "minecraft:air"},
{pos: [3, 4, 4], state: "minecraft:air"},
{pos: [4, 4, 0], state: "minecraft:air"},
{pos: [4, 4, 1], state: "minecraft:air"},
{pos: [4, 4, 2], state: "minecraft:air"},
{pos: [4, 4, 3], state: "minecraft:air"},
{pos: [4, 4, 4], state: "minecraft:air"}
],
entities: [],
palette: [
"minecraft:polished_andesite",
"minecraft:air",
"minecraft:chest{facing:north,type:single,waterlogged:false}",
"computercraft:computer_normal{facing:north,state:on}"
]
}

View File

@ -0,0 +1,139 @@
{
DataVersion: 3465,
size: [5, 5, 5],
data: [
{pos: [0, 0, 0], state: "minecraft:polished_andesite"},
{pos: [0, 0, 1], state: "minecraft:polished_andesite"},
{pos: [0, 0, 2], state: "minecraft:polished_andesite"},
{pos: [0, 0, 3], state: "minecraft:polished_andesite"},
{pos: [0, 0, 4], state: "minecraft:polished_andesite"},
{pos: [1, 0, 0], state: "minecraft:polished_andesite"},
{pos: [1, 0, 1], state: "minecraft:polished_andesite"},
{pos: [1, 0, 2], state: "minecraft:polished_andesite"},
{pos: [1, 0, 3], state: "minecraft:polished_andesite"},
{pos: [1, 0, 4], state: "minecraft:polished_andesite"},
{pos: [2, 0, 0], state: "minecraft:polished_andesite"},
{pos: [2, 0, 1], state: "minecraft:polished_andesite"},
{pos: [2, 0, 2], state: "minecraft:polished_andesite"},
{pos: [2, 0, 3], state: "minecraft:polished_andesite"},
{pos: [2, 0, 4], state: "minecraft:polished_andesite"},
{pos: [3, 0, 0], state: "minecraft:polished_andesite"},
{pos: [3, 0, 1], state: "minecraft:polished_andesite"},
{pos: [3, 0, 2], state: "minecraft:polished_andesite"},
{pos: [3, 0, 3], state: "minecraft:polished_andesite"},
{pos: [3, 0, 4], state: "minecraft:polished_andesite"},
{pos: [4, 0, 0], state: "minecraft:polished_andesite"},
{pos: [4, 0, 1], state: "minecraft:polished_andesite"},
{pos: [4, 0, 2], state: "minecraft:polished_andesite"},
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
{pos: [0, 1, 0], state: "minecraft:air"},
{pos: [0, 1, 1], state: "minecraft:air"},
{pos: [0, 1, 2], state: "minecraft:air"},
{pos: [0, 1, 3], state: "minecraft:air"},
{pos: [0, 1, 4], state: "minecraft:air"},
{pos: [1, 1, 0], state: "minecraft:air"},
{pos: [1, 1, 1], state: "minecraft:air"},
{pos: [1, 1, 2], state: "computercraft:cable{cable:true,down:false,east:true,modem:east_off_peripheral,north:false,south:false,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 1b, PeripheralId: 0, PeripheralType: "computer", id: "computercraft:cable"}},
{pos: [1, 1, 3], state: "minecraft:air"},
{pos: [1, 1, 4], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"},
{pos: [2, 1, 1], state: "minecraft:air"},
{pos: [2, 1, 2], state: "computercraft:computer_normal{facing:north,state:on}", nbt: {ComputerId: 1, Label: "modem_test.cable_modem_does_not_report_self", On: 1b, id: "computercraft:computer_normal"}},
{pos: [2, 1, 3], state: "computercraft:cable{cable:true,down:false,east:false,modem:north_off,north:true,south:false,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
{pos: [2, 1, 4], state: "minecraft:air"},
{pos: [3, 1, 0], state: "minecraft:air"},
{pos: [3, 1, 1], state: "minecraft:air"},
{pos: [3, 1, 2], state: "minecraft:air"},
{pos: [3, 1, 3], state: "minecraft:air"},
{pos: [3, 1, 4], state: "minecraft:air"},
{pos: [4, 1, 0], state: "minecraft:air"},
{pos: [4, 1, 1], state: "minecraft:air"},
{pos: [4, 1, 2], state: "minecraft:air"},
{pos: [4, 1, 3], state: "minecraft:air"},
{pos: [4, 1, 4], state: "minecraft:air"},
{pos: [0, 2, 0], state: "minecraft:air"},
{pos: [0, 2, 1], state: "minecraft:air"},
{pos: [0, 2, 2], state: "minecraft:air"},
{pos: [0, 2, 3], state: "minecraft:air"},
{pos: [0, 2, 4], state: "minecraft:air"},
{pos: [1, 2, 0], state: "minecraft:air"},
{pos: [1, 2, 1], state: "minecraft:air"},
{pos: [1, 2, 2], state: "minecraft:air"},
{pos: [1, 2, 3], state: "minecraft:air"},
{pos: [1, 2, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:air"},
{pos: [2, 2, 2], state: "minecraft:air"},
{pos: [2, 2, 3], state: "minecraft:air"},
{pos: [2, 2, 4], state: "minecraft:air"},
{pos: [3, 2, 0], state: "minecraft:air"},
{pos: [3, 2, 1], state: "minecraft:air"},
{pos: [3, 2, 2], state: "minecraft:air"},
{pos: [3, 2, 3], state: "minecraft:air"},
{pos: [3, 2, 4], state: "minecraft:air"},
{pos: [4, 2, 0], state: "minecraft:air"},
{pos: [4, 2, 1], state: "minecraft:air"},
{pos: [4, 2, 2], state: "minecraft:air"},
{pos: [4, 2, 3], state: "minecraft:air"},
{pos: [4, 2, 4], state: "minecraft:air"},
{pos: [0, 3, 0], state: "minecraft:air"},
{pos: [0, 3, 1], state: "minecraft:air"},
{pos: [0, 3, 2], state: "minecraft:air"},
{pos: [0, 3, 3], state: "minecraft:air"},
{pos: [0, 3, 4], state: "minecraft:air"},
{pos: [1, 3, 0], state: "minecraft:air"},
{pos: [1, 3, 1], state: "minecraft:air"},
{pos: [1, 3, 2], state: "minecraft:air"},
{pos: [1, 3, 3], state: "minecraft:air"},
{pos: [1, 3, 4], state: "minecraft:air"},
{pos: [2, 3, 0], state: "minecraft:air"},
{pos: [2, 3, 1], state: "minecraft:air"},
{pos: [2, 3, 2], state: "minecraft:air"},
{pos: [2, 3, 3], state: "minecraft:air"},
{pos: [2, 3, 4], state: "minecraft:air"},
{pos: [3, 3, 0], state: "minecraft:air"},
{pos: [3, 3, 1], state: "minecraft:air"},
{pos: [3, 3, 2], state: "minecraft:air"},
{pos: [3, 3, 3], state: "minecraft:air"},
{pos: [3, 3, 4], state: "minecraft:air"},
{pos: [4, 3, 0], state: "minecraft:air"},
{pos: [4, 3, 1], state: "minecraft:air"},
{pos: [4, 3, 2], state: "minecraft:air"},
{pos: [4, 3, 3], state: "minecraft:air"},
{pos: [4, 3, 4], state: "minecraft:air"},
{pos: [0, 4, 0], state: "minecraft:air"},
{pos: [0, 4, 1], state: "minecraft:air"},
{pos: [0, 4, 2], state: "minecraft:air"},
{pos: [0, 4, 3], state: "minecraft:air"},
{pos: [0, 4, 4], state: "minecraft:air"},
{pos: [1, 4, 0], state: "minecraft:air"},
{pos: [1, 4, 1], state: "minecraft:air"},
{pos: [1, 4, 2], state: "minecraft:air"},
{pos: [1, 4, 3], state: "minecraft:air"},
{pos: [1, 4, 4], state: "minecraft:air"},
{pos: [2, 4, 0], state: "minecraft:air"},
{pos: [2, 4, 1], state: "minecraft:air"},
{pos: [2, 4, 2], state: "minecraft:air"},
{pos: [2, 4, 3], state: "minecraft:air"},
{pos: [2, 4, 4], state: "minecraft:air"},
{pos: [3, 4, 0], state: "minecraft:air"},
{pos: [3, 4, 1], state: "minecraft:air"},
{pos: [3, 4, 2], state: "minecraft:air"},
{pos: [3, 4, 3], state: "minecraft:air"},
{pos: [3, 4, 4], state: "minecraft:air"},
{pos: [4, 4, 0], state: "minecraft:air"},
{pos: [4, 4, 1], state: "minecraft:air"},
{pos: [4, 4, 2], state: "minecraft:air"},
{pos: [4, 4, 3], state: "minecraft:air"},
{pos: [4, 4, 4], state: "minecraft:air"}
],
entities: [],
palette: [
"minecraft:polished_andesite",
"minecraft:air",
"computercraft:cable{cable:true,down:false,east:true,modem:east_off_peripheral,north:false,south:false,up:false,waterlogged:false,west:false}",
"computercraft:computer_normal{facing:north,state:on}",
"computercraft:cable{cable:true,down:false,east:false,modem:north_off,north:true,south:false,up:false,waterlogged:false,west:false}"
]
}

View File

@ -0,0 +1,139 @@
{
DataVersion: 3465,
size: [5, 5, 5],
data: [
{pos: [0, 0, 0], state: "minecraft:polished_andesite"},
{pos: [0, 0, 1], state: "minecraft:polished_andesite"},
{pos: [0, 0, 2], state: "minecraft:polished_andesite"},
{pos: [0, 0, 3], state: "minecraft:polished_andesite"},
{pos: [0, 0, 4], state: "minecraft:polished_andesite"},
{pos: [1, 0, 0], state: "minecraft:polished_andesite"},
{pos: [1, 0, 1], state: "minecraft:polished_andesite"},
{pos: [1, 0, 2], state: "minecraft:polished_andesite"},
{pos: [1, 0, 3], state: "minecraft:polished_andesite"},
{pos: [1, 0, 4], state: "minecraft:polished_andesite"},
{pos: [2, 0, 0], state: "minecraft:polished_andesite"},
{pos: [2, 0, 1], state: "minecraft:polished_andesite"},
{pos: [2, 0, 2], state: "minecraft:polished_andesite"},
{pos: [2, 0, 3], state: "minecraft:polished_andesite"},
{pos: [2, 0, 4], state: "minecraft:polished_andesite"},
{pos: [3, 0, 0], state: "minecraft:polished_andesite"},
{pos: [3, 0, 1], state: "minecraft:polished_andesite"},
{pos: [3, 0, 2], state: "minecraft:polished_andesite"},
{pos: [3, 0, 3], state: "minecraft:polished_andesite"},
{pos: [3, 0, 4], state: "minecraft:polished_andesite"},
{pos: [4, 0, 0], state: "minecraft:polished_andesite"},
{pos: [4, 0, 1], state: "minecraft:polished_andesite"},
{pos: [4, 0, 2], state: "minecraft:polished_andesite"},
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
{pos: [0, 1, 0], state: "minecraft:air"},
{pos: [0, 1, 1], state: "minecraft:air"},
{pos: [0, 1, 2], state: "minecraft:air"},
{pos: [0, 1, 3], state: "minecraft:air"},
{pos: [0, 1, 4], state: "minecraft:air"},
{pos: [1, 1, 0], state: "minecraft:air"},
{pos: [1, 1, 1], state: "minecraft:air"},
{pos: [1, 1, 2], state: "minecraft:air"},
{pos: [1, 1, 3], state: "minecraft:air"},
{pos: [1, 1, 4], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"},
{pos: [2, 1, 1], state: "minecraft:air"},
{pos: [2, 1, 2], state: "minecraft:chest{facing:north,type:single,waterlogged:false}", nbt: {Items: [], id: "minecraft:chest"}},
{pos: [2, 1, 3], state: "minecraft:air"},
{pos: [2, 1, 4], state: "minecraft:air"},
{pos: [3, 1, 0], state: "minecraft:air"},
{pos: [3, 1, 1], state: "computercraft:computer_normal{facing:north,state:on}", nbt: {ComputerId: 1, Label: "modem_test.chest_resizes_on_change", On: 1b, id: "computercraft:computer_normal"}},
{pos: [3, 1, 2], state: "computercraft:wired_modem_full{modem:false,peripheral:true}", nbt: {PeripheralId2: 2, PeripheralId4: 0, PeripheralType2: "computer", PeripheralType4: "minecraft:chest", id: "computercraft:wired_modem_full"}},
{pos: [3, 1, 3], state: "minecraft:air"},
{pos: [3, 1, 4], state: "minecraft:air"},
{pos: [4, 1, 0], state: "minecraft:air"},
{pos: [4, 1, 1], state: "minecraft:air"},
{pos: [4, 1, 2], state: "minecraft:air"},
{pos: [4, 1, 3], state: "minecraft:air"},
{pos: [4, 1, 4], state: "minecraft:air"},
{pos: [0, 2, 0], state: "minecraft:air"},
{pos: [0, 2, 1], state: "minecraft:air"},
{pos: [0, 2, 2], state: "minecraft:air"},
{pos: [0, 2, 3], state: "minecraft:air"},
{pos: [0, 2, 4], state: "minecraft:air"},
{pos: [1, 2, 0], state: "minecraft:air"},
{pos: [1, 2, 1], state: "minecraft:air"},
{pos: [1, 2, 2], state: "minecraft:air"},
{pos: [1, 2, 3], state: "minecraft:air"},
{pos: [1, 2, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:air"},
{pos: [2, 2, 2], state: "minecraft:air"},
{pos: [2, 2, 3], state: "minecraft:air"},
{pos: [2, 2, 4], state: "minecraft:air"},
{pos: [3, 2, 0], state: "minecraft:air"},
{pos: [3, 2, 1], state: "minecraft:air"},
{pos: [3, 2, 2], state: "minecraft:air"},
{pos: [3, 2, 3], state: "minecraft:air"},
{pos: [3, 2, 4], state: "minecraft:air"},
{pos: [4, 2, 0], state: "minecraft:air"},
{pos: [4, 2, 1], state: "minecraft:air"},
{pos: [4, 2, 2], state: "minecraft:air"},
{pos: [4, 2, 3], state: "minecraft:air"},
{pos: [4, 2, 4], state: "minecraft:air"},
{pos: [0, 3, 0], state: "minecraft:air"},
{pos: [0, 3, 1], state: "minecraft:air"},
{pos: [0, 3, 2], state: "minecraft:air"},
{pos: [0, 3, 3], state: "minecraft:air"},
{pos: [0, 3, 4], state: "minecraft:air"},
{pos: [1, 3, 0], state: "minecraft:air"},
{pos: [1, 3, 1], state: "minecraft:air"},
{pos: [1, 3, 2], state: "minecraft:air"},
{pos: [1, 3, 3], state: "minecraft:air"},
{pos: [1, 3, 4], state: "minecraft:air"},
{pos: [2, 3, 0], state: "minecraft:air"},
{pos: [2, 3, 1], state: "minecraft:air"},
{pos: [2, 3, 2], state: "minecraft:air"},
{pos: [2, 3, 3], state: "minecraft:air"},
{pos: [2, 3, 4], state: "minecraft:air"},
{pos: [3, 3, 0], state: "minecraft:air"},
{pos: [3, 3, 1], state: "minecraft:air"},
{pos: [3, 3, 2], state: "minecraft:air"},
{pos: [3, 3, 3], state: "minecraft:air"},
{pos: [3, 3, 4], state: "minecraft:air"},
{pos: [4, 3, 0], state: "minecraft:air"},
{pos: [4, 3, 1], state: "minecraft:air"},
{pos: [4, 3, 2], state: "minecraft:air"},
{pos: [4, 3, 3], state: "minecraft:air"},
{pos: [4, 3, 4], state: "minecraft:air"},
{pos: [0, 4, 0], state: "minecraft:air"},
{pos: [0, 4, 1], state: "minecraft:air"},
{pos: [0, 4, 2], state: "minecraft:air"},
{pos: [0, 4, 3], state: "minecraft:air"},
{pos: [0, 4, 4], state: "minecraft:air"},
{pos: [1, 4, 0], state: "minecraft:air"},
{pos: [1, 4, 1], state: "minecraft:air"},
{pos: [1, 4, 2], state: "minecraft:air"},
{pos: [1, 4, 3], state: "minecraft:air"},
{pos: [1, 4, 4], state: "minecraft:air"},
{pos: [2, 4, 0], state: "minecraft:air"},
{pos: [2, 4, 1], state: "minecraft:air"},
{pos: [2, 4, 2], state: "minecraft:air"},
{pos: [2, 4, 3], state: "minecraft:air"},
{pos: [2, 4, 4], state: "minecraft:air"},
{pos: [3, 4, 0], state: "minecraft:air"},
{pos: [3, 4, 1], state: "minecraft:air"},
{pos: [3, 4, 2], state: "minecraft:air"},
{pos: [3, 4, 3], state: "minecraft:air"},
{pos: [3, 4, 4], state: "minecraft:air"},
{pos: [4, 4, 0], state: "minecraft:air"},
{pos: [4, 4, 1], state: "minecraft:air"},
{pos: [4, 4, 2], state: "minecraft:air"},
{pos: [4, 4, 3], state: "minecraft:air"},
{pos: [4, 4, 4], state: "minecraft:air"}
],
entities: [],
palette: [
"minecraft:polished_andesite",
"minecraft:air",
"minecraft:chest{facing:north,type:single,waterlogged:false}",
"computercraft:computer_normal{facing:north,state:on}",
"computercraft:wired_modem_full{modem:false,peripheral:true}"
]
}

View File

@ -0,0 +1,139 @@
{
DataVersion: 3465,
size: [5, 5, 5],
data: [
{pos: [0, 0, 0], state: "minecraft:polished_andesite"},
{pos: [0, 0, 1], state: "minecraft:polished_andesite"},
{pos: [0, 0, 2], state: "minecraft:polished_andesite"},
{pos: [0, 0, 3], state: "minecraft:polished_andesite"},
{pos: [0, 0, 4], state: "minecraft:polished_andesite"},
{pos: [1, 0, 0], state: "minecraft:polished_andesite"},
{pos: [1, 0, 1], state: "minecraft:polished_andesite"},
{pos: [1, 0, 2], state: "minecraft:polished_andesite"},
{pos: [1, 0, 3], state: "minecraft:polished_andesite"},
{pos: [1, 0, 4], state: "minecraft:polished_andesite"},
{pos: [2, 0, 0], state: "minecraft:polished_andesite"},
{pos: [2, 0, 1], state: "minecraft:polished_andesite"},
{pos: [2, 0, 2], state: "minecraft:polished_andesite"},
{pos: [2, 0, 3], state: "minecraft:polished_andesite"},
{pos: [2, 0, 4], state: "minecraft:polished_andesite"},
{pos: [3, 0, 0], state: "minecraft:polished_andesite"},
{pos: [3, 0, 1], state: "minecraft:polished_andesite"},
{pos: [3, 0, 2], state: "minecraft:polished_andesite"},
{pos: [3, 0, 3], state: "minecraft:polished_andesite"},
{pos: [3, 0, 4], state: "minecraft:polished_andesite"},
{pos: [4, 0, 0], state: "minecraft:polished_andesite"},
{pos: [4, 0, 1], state: "minecraft:polished_andesite"},
{pos: [4, 0, 2], state: "minecraft:polished_andesite"},
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
{pos: [0, 1, 0], state: "minecraft:air"},
{pos: [0, 1, 1], state: "minecraft:air"},
{pos: [0, 1, 2], state: "minecraft:air"},
{pos: [0, 1, 3], state: "minecraft:air"},
{pos: [0, 1, 4], state: "minecraft:air"},
{pos: [1, 1, 0], state: "minecraft:air"},
{pos: [1, 1, 1], state: "minecraft:air"},
{pos: [1, 1, 2], state: "computercraft:wired_modem_full{modem:false,peripheral:true}", nbt: {PeripheralAccess: 1b, PeripheralId5: 1, PeripheralType5: "computer", id: "computercraft:wired_modem_full"}},
{pos: [1, 1, 3], state: "minecraft:air"},
{pos: [1, 1, 4], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"},
{pos: [2, 1, 1], state: "minecraft:air"},
{pos: [2, 1, 2], state: "computercraft:computer_normal{facing:north,state:on}", nbt: {ComputerId: 1, Label: "modem_test.full_block_modem_does_not_report_self", On: 1b, id: "computercraft:computer_normal"}},
{pos: [2, 1, 3], state: "computercraft:wired_modem_full{modem:false,peripheral:false}", nbt: {PeripheralAccess: 0b, id: "computercraft:wired_modem_full"}},
{pos: [2, 1, 4], state: "minecraft:air"},
{pos: [3, 1, 0], state: "minecraft:air"},
{pos: [3, 1, 1], state: "minecraft:air"},
{pos: [3, 1, 2], state: "minecraft:air"},
{pos: [3, 1, 3], state: "minecraft:air"},
{pos: [3, 1, 4], state: "minecraft:air"},
{pos: [4, 1, 0], state: "minecraft:air"},
{pos: [4, 1, 1], state: "minecraft:air"},
{pos: [4, 1, 2], state: "minecraft:air"},
{pos: [4, 1, 3], state: "minecraft:air"},
{pos: [4, 1, 4], state: "minecraft:air"},
{pos: [0, 2, 0], state: "minecraft:air"},
{pos: [0, 2, 1], state: "minecraft:air"},
{pos: [0, 2, 2], state: "minecraft:air"},
{pos: [0, 2, 3], state: "minecraft:air"},
{pos: [0, 2, 4], state: "minecraft:air"},
{pos: [1, 2, 0], state: "minecraft:air"},
{pos: [1, 2, 1], state: "minecraft:air"},
{pos: [1, 2, 2], state: "minecraft:air"},
{pos: [1, 2, 3], state: "minecraft:air"},
{pos: [1, 2, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:air"},
{pos: [2, 2, 2], state: "minecraft:air"},
{pos: [2, 2, 3], state: "minecraft:air"},
{pos: [2, 2, 4], state: "minecraft:air"},
{pos: [3, 2, 0], state: "minecraft:air"},
{pos: [3, 2, 1], state: "minecraft:air"},
{pos: [3, 2, 2], state: "minecraft:air"},
{pos: [3, 2, 3], state: "minecraft:air"},
{pos: [3, 2, 4], state: "minecraft:air"},
{pos: [4, 2, 0], state: "minecraft:air"},
{pos: [4, 2, 1], state: "minecraft:air"},
{pos: [4, 2, 2], state: "minecraft:air"},
{pos: [4, 2, 3], state: "minecraft:air"},
{pos: [4, 2, 4], state: "minecraft:air"},
{pos: [0, 3, 0], state: "minecraft:air"},
{pos: [0, 3, 1], state: "minecraft:air"},
{pos: [0, 3, 2], state: "minecraft:air"},
{pos: [0, 3, 3], state: "minecraft:air"},
{pos: [0, 3, 4], state: "minecraft:air"},
{pos: [1, 3, 0], state: "minecraft:air"},
{pos: [1, 3, 1], state: "minecraft:air"},
{pos: [1, 3, 2], state: "minecraft:air"},
{pos: [1, 3, 3], state: "minecraft:air"},
{pos: [1, 3, 4], state: "minecraft:air"},
{pos: [2, 3, 0], state: "minecraft:air"},
{pos: [2, 3, 1], state: "minecraft:air"},
{pos: [2, 3, 2], state: "minecraft:air"},
{pos: [2, 3, 3], state: "minecraft:air"},
{pos: [2, 3, 4], state: "minecraft:air"},
{pos: [3, 3, 0], state: "minecraft:air"},
{pos: [3, 3, 1], state: "minecraft:air"},
{pos: [3, 3, 2], state: "minecraft:air"},
{pos: [3, 3, 3], state: "minecraft:air"},
{pos: [3, 3, 4], state: "minecraft:air"},
{pos: [4, 3, 0], state: "minecraft:air"},
{pos: [4, 3, 1], state: "minecraft:air"},
{pos: [4, 3, 2], state: "minecraft:air"},
{pos: [4, 3, 3], state: "minecraft:air"},
{pos: [4, 3, 4], state: "minecraft:air"},
{pos: [0, 4, 0], state: "minecraft:air"},
{pos: [0, 4, 1], state: "minecraft:air"},
{pos: [0, 4, 2], state: "minecraft:air"},
{pos: [0, 4, 3], state: "minecraft:air"},
{pos: [0, 4, 4], state: "minecraft:air"},
{pos: [1, 4, 0], state: "minecraft:air"},
{pos: [1, 4, 1], state: "minecraft:air"},
{pos: [1, 4, 2], state: "minecraft:air"},
{pos: [1, 4, 3], state: "minecraft:air"},
{pos: [1, 4, 4], state: "minecraft:air"},
{pos: [2, 4, 0], state: "minecraft:air"},
{pos: [2, 4, 1], state: "minecraft:air"},
{pos: [2, 4, 2], state: "minecraft:air"},
{pos: [2, 4, 3], state: "minecraft:air"},
{pos: [2, 4, 4], state: "minecraft:air"},
{pos: [3, 4, 0], state: "minecraft:air"},
{pos: [3, 4, 1], state: "minecraft:air"},
{pos: [3, 4, 2], state: "minecraft:air"},
{pos: [3, 4, 3], state: "minecraft:air"},
{pos: [3, 4, 4], state: "minecraft:air"},
{pos: [4, 4, 0], state: "minecraft:air"},
{pos: [4, 4, 1], state: "minecraft:air"},
{pos: [4, 4, 2], state: "minecraft:air"},
{pos: [4, 4, 3], state: "minecraft:air"},
{pos: [4, 4, 4], state: "minecraft:air"}
],
entities: [],
palette: [
"minecraft:polished_andesite",
"minecraft:air",
"computercraft:wired_modem_full{modem:false,peripheral:true}",
"computercraft:computer_normal{facing:north,state:on}",
"computercraft:wired_modem_full{modem:false,peripheral:false}"
]
}

View File

@ -28,7 +28,7 @@
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
{pos: [4, 0, 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}"

View File

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

View File

@ -0,0 +1,138 @@
{
DataVersion: 3465,
size: [5, 5, 5],
data: [
{pos: [0, 0, 0], state: "minecraft:polished_andesite"},
{pos: [0, 0, 1], state: "minecraft:polished_andesite"},
{pos: [0, 0, 2], state: "minecraft:polished_andesite"},
{pos: [0, 0, 3], state: "minecraft:polished_andesite"},
{pos: [0, 0, 4], state: "minecraft:polished_andesite"},
{pos: [1, 0, 0], state: "minecraft:polished_andesite"},
{pos: [1, 0, 1], state: "minecraft:polished_andesite"},
{pos: [1, 0, 2], state: "minecraft:polished_andesite"},
{pos: [1, 0, 3], state: "minecraft:polished_andesite"},
{pos: [1, 0, 4], state: "minecraft:polished_andesite"},
{pos: [2, 0, 0], state: "minecraft:polished_andesite"},
{pos: [2, 0, 1], state: "minecraft:polished_andesite"},
{pos: [2, 0, 2], state: "minecraft:polished_andesite"},
{pos: [2, 0, 3], state: "minecraft:polished_andesite"},
{pos: [2, 0, 4], state: "minecraft:polished_andesite"},
{pos: [3, 0, 0], state: "minecraft:polished_andesite"},
{pos: [3, 0, 1], state: "minecraft:polished_andesite"},
{pos: [3, 0, 2], state: "minecraft:polished_andesite"},
{pos: [3, 0, 3], state: "minecraft:polished_andesite"},
{pos: [3, 0, 4], state: "minecraft:polished_andesite"},
{pos: [4, 0, 0], state: "minecraft:polished_andesite"},
{pos: [4, 0, 1], state: "minecraft:polished_andesite"},
{pos: [4, 0, 2], state: "minecraft:polished_andesite"},
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
{pos: [0, 1, 0], state: "minecraft:air"},
{pos: [0, 1, 1], state: "minecraft:air"},
{pos: [0, 1, 2], state: "minecraft:air"},
{pos: [0, 1, 3], state: "minecraft:air"},
{pos: [0, 1, 4], state: "minecraft:air"},
{pos: [1, 1, 0], state: "minecraft:air"},
{pos: [1, 1, 1], state: "minecraft:light_gray_stained_glass"},
{pos: [1, 1, 2], state: "minecraft:light_gray_stained_glass"},
{pos: [1, 1, 3], state: "minecraft:light_gray_stained_glass"},
{pos: [1, 1, 4], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"},
{pos: [2, 1, 1], state: "minecraft:light_gray_stained_glass"},
{pos: [2, 1, 2], state: "computercraft:cable{cable:false,down:false,east:false,modem:up_off,north:false,south:false,up:false,waterlogged:false,west:false}", nbt: {id: "computercraft:cable"}},
{pos: [2, 1, 3], state: "minecraft:light_gray_stained_glass"},
{pos: [2, 1, 4], state: "minecraft:air"},
{pos: [3, 1, 0], state: "minecraft:air"},
{pos: [3, 1, 1], state: "minecraft:light_gray_stained_glass"},
{pos: [3, 1, 2], state: "minecraft:light_gray_stained_glass"},
{pos: [3, 1, 3], state: "minecraft:light_gray_stained_glass"},
{pos: [3, 1, 4], state: "minecraft:air"},
{pos: [4, 1, 0], state: "minecraft:air"},
{pos: [4, 1, 1], state: "minecraft:air"},
{pos: [4, 1, 2], state: "minecraft:air"},
{pos: [4, 1, 3], state: "minecraft:air"},
{pos: [4, 1, 4], state: "minecraft:air"},
{pos: [0, 2, 0], state: "minecraft:air"},
{pos: [0, 2, 1], state: "minecraft:air"},
{pos: [0, 2, 2], state: "minecraft:air"},
{pos: [0, 2, 3], state: "minecraft:air"},
{pos: [0, 2, 4], state: "minecraft:air"},
{pos: [1, 2, 0], state: "minecraft:air"},
{pos: [1, 2, 1], state: "minecraft:light_gray_stained_glass"},
{pos: [1, 2, 2], state: "minecraft:light_gray_stained_glass"},
{pos: [1, 2, 3], state: "minecraft:light_gray_stained_glass"},
{pos: [1, 2, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:light_gray_stained_glass"},
{pos: [2, 2, 2], state: "minecraft:light_gray_stained_glass"},
{pos: [2, 2, 3], state: "minecraft:light_gray_stained_glass"},
{pos: [2, 2, 4], state: "minecraft:air"},
{pos: [3, 2, 0], state: "minecraft:air"},
{pos: [3, 2, 1], state: "minecraft:light_gray_stained_glass"},
{pos: [3, 2, 2], state: "minecraft:light_gray_stained_glass"},
{pos: [3, 2, 3], state: "minecraft:light_gray_stained_glass"},
{pos: [3, 2, 4], state: "minecraft:air"},
{pos: [4, 2, 0], state: "minecraft:air"},
{pos: [4, 2, 1], state: "minecraft:air"},
{pos: [4, 2, 2], state: "minecraft:air"},
{pos: [4, 2, 3], state: "minecraft:air"},
{pos: [4, 2, 4], state: "minecraft:air"},
{pos: [0, 3, 0], state: "minecraft:air"},
{pos: [0, 3, 1], state: "minecraft:air"},
{pos: [0, 3, 2], state: "minecraft:air"},
{pos: [0, 3, 3], state: "minecraft:air"},
{pos: [0, 3, 4], state: "minecraft:air"},
{pos: [1, 3, 0], state: "minecraft:air"},
{pos: [1, 3, 1], state: "minecraft:air"},
{pos: [1, 3, 2], state: "minecraft:air"},
{pos: [1, 3, 3], state: "minecraft:air"},
{pos: [1, 3, 4], state: "minecraft:air"},
{pos: [2, 3, 0], state: "minecraft:air"},
{pos: [2, 3, 1], state: "minecraft:air"},
{pos: [2, 3, 2], state: "minecraft:air"},
{pos: [2, 3, 3], state: "minecraft:air"},
{pos: [2, 3, 4], state: "minecraft:air"},
{pos: [3, 3, 0], state: "minecraft:air"},
{pos: [3, 3, 1], state: "minecraft:air"},
{pos: [3, 3, 2], state: "minecraft:air"},
{pos: [3, 3, 3], state: "minecraft:air"},
{pos: [3, 3, 4], state: "minecraft:air"},
{pos: [4, 3, 0], state: "minecraft:air"},
{pos: [4, 3, 1], state: "minecraft:air"},
{pos: [4, 3, 2], state: "minecraft:air"},
{pos: [4, 3, 3], state: "minecraft:air"},
{pos: [4, 3, 4], state: "minecraft:air"},
{pos: [0, 4, 0], state: "minecraft:air"},
{pos: [0, 4, 1], state: "minecraft:air"},
{pos: [0, 4, 2], state: "minecraft:air"},
{pos: [0, 4, 3], state: "minecraft:air"},
{pos: [0, 4, 4], state: "minecraft:air"},
{pos: [1, 4, 0], state: "minecraft:air"},
{pos: [1, 4, 1], state: "minecraft:air"},
{pos: [1, 4, 2], state: "minecraft:air"},
{pos: [1, 4, 3], state: "minecraft:air"},
{pos: [1, 4, 4], state: "minecraft:air"},
{pos: [2, 4, 0], state: "minecraft:air"},
{pos: [2, 4, 1], state: "minecraft:air"},
{pos: [2, 4, 2], state: "minecraft:air"},
{pos: [2, 4, 3], state: "minecraft:air"},
{pos: [2, 4, 4], state: "minecraft:air"},
{pos: [3, 4, 0], state: "minecraft:air"},
{pos: [3, 4, 1], state: "minecraft:air"},
{pos: [3, 4, 2], state: "minecraft:air"},
{pos: [3, 4, 3], state: "minecraft:air"},
{pos: [3, 4, 4], state: "minecraft:air"},
{pos: [4, 4, 0], state: "minecraft:air"},
{pos: [4, 4, 1], state: "minecraft:air"},
{pos: [4, 4, 2], state: "minecraft:air"},
{pos: [4, 4, 3], state: "minecraft:air"},
{pos: [4, 4, 4], state: "minecraft:air"}
],
entities: [],
palette: [
"minecraft:polished_andesite",
"minecraft:light_gray_stained_glass",
"minecraft:air",
"computercraft:cable{cable:false,down:false,east:false,modem:up_off,north:false,south:false,up:false,waterlogged:false,west:false}"
]
}

View File

@ -0,0 +1,138 @@
{
DataVersion: 3465,
size: [5, 5, 5],
data: [
{pos: [0, 0, 0], state: "minecraft:polished_andesite"},
{pos: [0, 0, 1], state: "minecraft:polished_andesite"},
{pos: [0, 0, 2], state: "minecraft:polished_andesite"},
{pos: [0, 0, 3], state: "minecraft:polished_andesite"},
{pos: [0, 0, 4], state: "minecraft:polished_andesite"},
{pos: [1, 0, 0], state: "minecraft:polished_andesite"},
{pos: [1, 0, 1], state: "minecraft:polished_andesite"},
{pos: [1, 0, 2], state: "minecraft:polished_andesite"},
{pos: [1, 0, 3], state: "minecraft:polished_andesite"},
{pos: [1, 0, 4], state: "minecraft:polished_andesite"},
{pos: [2, 0, 0], state: "minecraft:polished_andesite"},
{pos: [2, 0, 1], state: "minecraft:polished_andesite"},
{pos: [2, 0, 2], state: "minecraft:polished_andesite"},
{pos: [2, 0, 3], state: "minecraft:polished_andesite"},
{pos: [2, 0, 4], state: "minecraft:polished_andesite"},
{pos: [3, 0, 0], state: "minecraft:polished_andesite"},
{pos: [3, 0, 1], state: "minecraft:polished_andesite"},
{pos: [3, 0, 2], state: "minecraft:polished_andesite"},
{pos: [3, 0, 3], state: "minecraft:polished_andesite"},
{pos: [3, 0, 4], state: "minecraft:polished_andesite"},
{pos: [4, 0, 0], state: "minecraft:polished_andesite"},
{pos: [4, 0, 1], state: "minecraft:polished_andesite"},
{pos: [4, 0, 2], state: "minecraft:polished_andesite"},
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
{pos: [0, 1, 0], state: "minecraft:air"},
{pos: [0, 1, 1], state: "minecraft:air"},
{pos: [0, 1, 2], state: "minecraft:air"},
{pos: [0, 1, 3], state: "minecraft:air"},
{pos: [0, 1, 4], state: "minecraft:air"},
{pos: [1, 1, 0], state: "minecraft:air"},
{pos: [1, 1, 1], state: "minecraft:light_gray_stained_glass"},
{pos: [1, 1, 2], state: "minecraft:light_gray_stained_glass"},
{pos: [1, 1, 3], state: "minecraft:light_gray_stained_glass"},
{pos: [1, 1, 4], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"},
{pos: [2, 1, 1], state: "minecraft:light_gray_stained_glass"},
{pos: [2, 1, 2], state: "computercraft:cable{cable:true,down:false,east:false,modem:up_off,north:false,south:false,up:true,waterlogged:false,west:false}", nbt: {id: "computercraft:cable"}},
{pos: [2, 1, 3], state: "minecraft:light_gray_stained_glass"},
{pos: [2, 1, 4], state: "minecraft:air"},
{pos: [3, 1, 0], state: "minecraft:air"},
{pos: [3, 1, 1], state: "minecraft:light_gray_stained_glass"},
{pos: [3, 1, 2], state: "minecraft:light_gray_stained_glass"},
{pos: [3, 1, 3], state: "minecraft:light_gray_stained_glass"},
{pos: [3, 1, 4], state: "minecraft:air"},
{pos: [4, 1, 0], state: "minecraft:air"},
{pos: [4, 1, 1], state: "minecraft:air"},
{pos: [4, 1, 2], state: "minecraft:air"},
{pos: [4, 1, 3], state: "minecraft:air"},
{pos: [4, 1, 4], state: "minecraft:air"},
{pos: [0, 2, 0], state: "minecraft:air"},
{pos: [0, 2, 1], state: "minecraft:air"},
{pos: [0, 2, 2], state: "minecraft:air"},
{pos: [0, 2, 3], state: "minecraft:air"},
{pos: [0, 2, 4], state: "minecraft:air"},
{pos: [1, 2, 0], state: "minecraft:air"},
{pos: [1, 2, 1], state: "minecraft:light_gray_stained_glass"},
{pos: [1, 2, 2], state: "minecraft:light_gray_stained_glass"},
{pos: [1, 2, 3], state: "minecraft:light_gray_stained_glass"},
{pos: [1, 2, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:light_gray_stained_glass"},
{pos: [2, 2, 2], state: "minecraft:light_gray_stained_glass"},
{pos: [2, 2, 3], state: "minecraft:light_gray_stained_glass"},
{pos: [2, 2, 4], state: "minecraft:air"},
{pos: [3, 2, 0], state: "minecraft:air"},
{pos: [3, 2, 1], state: "minecraft:light_gray_stained_glass"},
{pos: [3, 2, 2], state: "minecraft:light_gray_stained_glass"},
{pos: [3, 2, 3], state: "minecraft:light_gray_stained_glass"},
{pos: [3, 2, 4], state: "minecraft:air"},
{pos: [4, 2, 0], state: "minecraft:air"},
{pos: [4, 2, 1], state: "minecraft:air"},
{pos: [4, 2, 2], state: "minecraft:air"},
{pos: [4, 2, 3], state: "minecraft:air"},
{pos: [4, 2, 4], state: "minecraft:air"},
{pos: [0, 3, 0], state: "minecraft:air"},
{pos: [0, 3, 1], state: "minecraft:air"},
{pos: [0, 3, 2], state: "minecraft:air"},
{pos: [0, 3, 3], state: "minecraft:air"},
{pos: [0, 3, 4], state: "minecraft:air"},
{pos: [1, 3, 0], state: "minecraft:air"},
{pos: [1, 3, 1], state: "minecraft:air"},
{pos: [1, 3, 2], state: "minecraft:air"},
{pos: [1, 3, 3], state: "minecraft:air"},
{pos: [1, 3, 4], state: "minecraft:air"},
{pos: [2, 3, 0], state: "minecraft:air"},
{pos: [2, 3, 1], state: "minecraft:air"},
{pos: [2, 3, 2], state: "minecraft:air"},
{pos: [2, 3, 3], state: "minecraft:air"},
{pos: [2, 3, 4], state: "minecraft:air"},
{pos: [3, 3, 0], state: "minecraft:air"},
{pos: [3, 3, 1], state: "minecraft:air"},
{pos: [3, 3, 2], state: "minecraft:air"},
{pos: [3, 3, 3], state: "minecraft:air"},
{pos: [3, 3, 4], state: "minecraft:air"},
{pos: [4, 3, 0], state: "minecraft:air"},
{pos: [4, 3, 1], state: "minecraft:air"},
{pos: [4, 3, 2], state: "minecraft:air"},
{pos: [4, 3, 3], state: "minecraft:air"},
{pos: [4, 3, 4], state: "minecraft:air"},
{pos: [0, 4, 0], state: "minecraft:air"},
{pos: [0, 4, 1], state: "minecraft:air"},
{pos: [0, 4, 2], state: "minecraft:air"},
{pos: [0, 4, 3], state: "minecraft:air"},
{pos: [0, 4, 4], state: "minecraft:air"},
{pos: [1, 4, 0], state: "minecraft:air"},
{pos: [1, 4, 1], state: "minecraft:air"},
{pos: [1, 4, 2], state: "minecraft:air"},
{pos: [1, 4, 3], state: "minecraft:air"},
{pos: [1, 4, 4], state: "minecraft:air"},
{pos: [2, 4, 0], state: "minecraft:air"},
{pos: [2, 4, 1], state: "minecraft:air"},
{pos: [2, 4, 2], state: "minecraft:air"},
{pos: [2, 4, 3], state: "minecraft:air"},
{pos: [2, 4, 4], state: "minecraft:air"},
{pos: [3, 4, 0], state: "minecraft:air"},
{pos: [3, 4, 1], state: "minecraft:air"},
{pos: [3, 4, 2], state: "minecraft:air"},
{pos: [3, 4, 3], state: "minecraft:air"},
{pos: [3, 4, 4], state: "minecraft:air"},
{pos: [4, 4, 0], state: "minecraft:air"},
{pos: [4, 4, 1], state: "minecraft:air"},
{pos: [4, 4, 2], state: "minecraft:air"},
{pos: [4, 4, 3], state: "minecraft:air"},
{pos: [4, 4, 4], state: "minecraft:air"}
],
entities: [],
palette: [
"minecraft:polished_andesite",
"minecraft:light_gray_stained_glass",
"minecraft:air",
"computercraft:cable{cable:true,down:false,east:false,modem:up_off,north:false,south:false,up:true,waterlogged:false,west:false}"
]
}

View File

@ -0,0 +1,138 @@
{
DataVersion: 3465,
size: [5, 5, 5],
data: [
{pos: [0, 0, 0], state: "minecraft:polished_andesite"},
{pos: [0, 0, 1], state: "minecraft:polished_andesite"},
{pos: [0, 0, 2], state: "minecraft:polished_andesite"},
{pos: [0, 0, 3], state: "minecraft:polished_andesite"},
{pos: [0, 0, 4], state: "minecraft:polished_andesite"},
{pos: [1, 0, 0], state: "minecraft:polished_andesite"},
{pos: [1, 0, 1], state: "minecraft:polished_andesite"},
{pos: [1, 0, 2], state: "minecraft:polished_andesite"},
{pos: [1, 0, 3], state: "minecraft:polished_andesite"},
{pos: [1, 0, 4], state: "minecraft:polished_andesite"},
{pos: [2, 0, 0], state: "minecraft:polished_andesite"},
{pos: [2, 0, 1], state: "minecraft:polished_andesite"},
{pos: [2, 0, 2], state: "minecraft:polished_andesite"},
{pos: [2, 0, 3], state: "minecraft:polished_andesite"},
{pos: [2, 0, 4], state: "minecraft:polished_andesite"},
{pos: [3, 0, 0], state: "minecraft:polished_andesite"},
{pos: [3, 0, 1], state: "minecraft:polished_andesite"},
{pos: [3, 0, 2], state: "minecraft:polished_andesite"},
{pos: [3, 0, 3], state: "minecraft:polished_andesite"},
{pos: [3, 0, 4], state: "minecraft:polished_andesite"},
{pos: [4, 0, 0], state: "minecraft:polished_andesite"},
{pos: [4, 0, 1], state: "minecraft:polished_andesite"},
{pos: [4, 0, 2], state: "minecraft:polished_andesite"},
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
{pos: [0, 1, 0], state: "minecraft:air"},
{pos: [0, 1, 1], state: "minecraft:air"},
{pos: [0, 1, 2], state: "minecraft:air"},
{pos: [0, 1, 3], state: "minecraft:air"},
{pos: [0, 1, 4], state: "minecraft:air"},
{pos: [1, 1, 0], state: "minecraft:air"},
{pos: [1, 1, 1], state: "minecraft:air"},
{pos: [1, 1, 2], state: "minecraft:air"},
{pos: [1, 1, 3], state: "minecraft:air"},
{pos: [1, 1, 4], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"},
{pos: [2, 1, 1], state: "minecraft:air"},
{pos: [2, 1, 2], state: "computercraft:turtle_normal{facing:north,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [], Label: "turtle_test.sided_suck", On: 1b, Slot: 0, id: "computercraft:turtle_normal"}},
{pos: [2, 1, 3], state: "minecraft:air"},
{pos: [2, 1, 4], state: "minecraft:air"},
{pos: [3, 1, 0], state: "minecraft:air"},
{pos: [3, 1, 1], state: "minecraft:air"},
{pos: [3, 1, 2], state: "minecraft:air"},
{pos: [3, 1, 3], state: "minecraft:air"},
{pos: [3, 1, 4], state: "minecraft:air"},
{pos: [4, 1, 0], state: "minecraft:air"},
{pos: [4, 1, 1], state: "minecraft:air"},
{pos: [4, 1, 2], state: "minecraft:air"},
{pos: [4, 1, 3], state: "minecraft:air"},
{pos: [4, 1, 4], state: "minecraft:air"},
{pos: [0, 2, 0], state: "minecraft:air"},
{pos: [0, 2, 1], state: "minecraft:air"},
{pos: [0, 2, 2], state: "minecraft:air"},
{pos: [0, 2, 3], state: "minecraft:air"},
{pos: [0, 2, 4], state: "minecraft:air"},
{pos: [1, 2, 0], state: "minecraft:air"},
{pos: [1, 2, 1], state: "minecraft:air"},
{pos: [1, 2, 2], state: "minecraft:air"},
{pos: [1, 2, 3], state: "minecraft:air"},
{pos: [1, 2, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:air"},
{pos: [2, 2, 2], state: "minecraft:furnace{facing:north,lit:false}", nbt: {BurnTime: 0s, CookTime: 0s, CookTimeTotal: 200s, Items: [{Count: 64b, Slot: 1b, id: "minecraft:coal"}, {Count: 8b, Slot: 2b, id: "minecraft:iron_ingot"}], RecipesUsed: {"minecraft:iron_ingot_from_smelting_raw_iron": 8}, id: "minecraft:furnace"}},
{pos: [2, 2, 3], state: "minecraft:air"},
{pos: [2, 2, 4], state: "minecraft:air"},
{pos: [3, 2, 0], state: "minecraft:air"},
{pos: [3, 2, 1], state: "minecraft:air"},
{pos: [3, 2, 2], state: "minecraft:air"},
{pos: [3, 2, 3], state: "minecraft:air"},
{pos: [3, 2, 4], state: "minecraft:air"},
{pos: [4, 2, 0], state: "minecraft:air"},
{pos: [4, 2, 1], state: "minecraft:air"},
{pos: [4, 2, 2], state: "minecraft:air"},
{pos: [4, 2, 3], state: "minecraft:air"},
{pos: [4, 2, 4], state: "minecraft:air"},
{pos: [0, 3, 0], state: "minecraft:air"},
{pos: [0, 3, 1], state: "minecraft:air"},
{pos: [0, 3, 2], state: "minecraft:air"},
{pos: [0, 3, 3], state: "minecraft:air"},
{pos: [0, 3, 4], state: "minecraft:air"},
{pos: [1, 3, 0], state: "minecraft:air"},
{pos: [1, 3, 1], state: "minecraft:air"},
{pos: [1, 3, 2], state: "minecraft:air"},
{pos: [1, 3, 3], state: "minecraft:air"},
{pos: [1, 3, 4], state: "minecraft:air"},
{pos: [2, 3, 0], state: "minecraft:air"},
{pos: [2, 3, 1], state: "minecraft:air"},
{pos: [2, 3, 2], state: "minecraft:air"},
{pos: [2, 3, 3], state: "minecraft:air"},
{pos: [2, 3, 4], state: "minecraft:air"},
{pos: [3, 3, 0], state: "minecraft:air"},
{pos: [3, 3, 1], state: "minecraft:air"},
{pos: [3, 3, 2], state: "minecraft:air"},
{pos: [3, 3, 3], state: "minecraft:air"},
{pos: [3, 3, 4], state: "minecraft:air"},
{pos: [4, 3, 0], state: "minecraft:air"},
{pos: [4, 3, 1], state: "minecraft:air"},
{pos: [4, 3, 2], state: "minecraft:air"},
{pos: [4, 3, 3], state: "minecraft:air"},
{pos: [4, 3, 4], state: "minecraft:air"},
{pos: [0, 4, 0], state: "minecraft:air"},
{pos: [0, 4, 1], state: "minecraft:air"},
{pos: [0, 4, 2], state: "minecraft:air"},
{pos: [0, 4, 3], state: "minecraft:air"},
{pos: [0, 4, 4], state: "minecraft:air"},
{pos: [1, 4, 0], state: "minecraft:air"},
{pos: [1, 4, 1], state: "minecraft:air"},
{pos: [1, 4, 2], state: "minecraft:air"},
{pos: [1, 4, 3], state: "minecraft:air"},
{pos: [1, 4, 4], state: "minecraft:air"},
{pos: [2, 4, 0], state: "minecraft:air"},
{pos: [2, 4, 1], state: "minecraft:air"},
{pos: [2, 4, 2], state: "minecraft:air"},
{pos: [2, 4, 3], state: "minecraft:air"},
{pos: [2, 4, 4], state: "minecraft:air"},
{pos: [3, 4, 0], state: "minecraft:air"},
{pos: [3, 4, 1], state: "minecraft:air"},
{pos: [3, 4, 2], state: "minecraft:air"},
{pos: [3, 4, 3], state: "minecraft:air"},
{pos: [3, 4, 4], state: "minecraft:air"},
{pos: [4, 4, 0], state: "minecraft:air"},
{pos: [4, 4, 1], state: "minecraft:air"},
{pos: [4, 4, 2], state: "minecraft:air"},
{pos: [4, 4, 3], state: "minecraft:air"},
{pos: [4, 4, 4], state: "minecraft:air"}
],
entities: [],
palette: [
"minecraft:polished_andesite",
"minecraft:air",
"computercraft:turtle_normal{facing:north,waterlogged:false}",
"minecraft:furnace{facing:north,lit:false}"
]
}

View File

@ -116,6 +116,7 @@ public static MethodResult pullEventRaw(@Nullable String filter, ILuaCallback ca
* @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);

View File

@ -11,6 +11,7 @@
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 @@
* @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 @@ private class PeripheralWrapper extends ComputerAccess {
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 MethodResult call(ILuaContext context, String methodName, IArguments argu
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

View File

@ -13,8 +13,54 @@
/**
* 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
*/

View File

@ -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 @@
* @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) {
}

View File

@ -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) {

View File

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

View File

@ -15,8 +15,7 @@
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 String getLabel() {
}
public void setLabel(@Nullable String label) {
if (!Objects.equals(label, this.label)) {
this.label = label;
externalOutputChanged.set(true);
}
this.label = label;
}
public void tick() {
@ -164,28 +160,24 @@ public void tick() {
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