register, Minecraft minecraft) {
@@ -165,17 +179,14 @@ public final class ClientRegistry {
}
private static int getPocketColour(ItemStack stack, int layer) {
- switch (layer) {
- case 0:
- default:
- return 0xFFFFFF;
- case 1: // Frame colour
- return IColouredItem.getColourBasic(stack);
- case 2: { // Light colour
- var light = ClientPocketComputers.get(stack).getLightState();
- return light == -1 ? Colour.BLACK.getHex() : light;
+ return switch (layer) {
+ default -> 0xFFFFFF;
+ case 1 -> IColouredItem.getColourBasic(stack); // Frame colour
+ case 2 -> { // Light colour
+ var computer = ClientPocketComputers.get(stack);
+ yield computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState();
}
- }
+ };
}
private static int getTurtleColour(ItemStack stack, int layer) {
diff --git a/projects/common/src/client/java/dan200/computercraft/client/platform/ClientNetworkContextImpl.java b/projects/common/src/client/java/dan200/computercraft/client/platform/ClientNetworkContextImpl.java
index 941c56c73..2f1708e90 100644
--- a/projects/common/src/client/java/dan200/computercraft/client/platform/ClientNetworkContextImpl.java
+++ b/projects/common/src/client/java/dan200/computercraft/client/platform/ClientNetworkContextImpl.java
@@ -17,6 +17,7 @@ import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.computer.upload.UploadResult;
import dan200.computercraft.shared.network.client.ClientNetworkContext;
import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity;
+import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
@@ -27,7 +28,6 @@ import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import javax.annotation.Nullable;
-import java.nio.ByteBuffer;
import java.util.UUID;
/**
@@ -67,19 +67,17 @@ public final class ClientNetworkContextImpl implements ClientNetworkContext {
}
@Override
- public void handlePocketComputerData(int instanceId, ComputerState state, int lightState, TerminalState terminal) {
- var computer = ClientPocketComputers.get(instanceId, terminal.colour);
- computer.setState(state, lightState);
- if (terminal.hasTerminal()) computer.setTerminal(terminal);
+ public void handlePocketComputerData(UUID instanceId, ComputerState state, int lightState, TerminalState terminal) {
+ ClientPocketComputers.setState(instanceId, state, lightState, terminal);
}
@Override
- public void handlePocketComputerDeleted(int instanceId) {
+ public void handlePocketComputerDeleted(UUID instanceId) {
ClientPocketComputers.remove(instanceId);
}
@Override
- public void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, ByteBuffer buffer) {
+ public void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, EncodedAudio buffer) {
SpeakerManager.getSound(source).playAudio(reifyPosition(position), volume, buffer);
}
diff --git a/projects/common/src/client/java/dan200/computercraft/client/pocket/ClientPocketComputers.java b/projects/common/src/client/java/dan200/computercraft/client/pocket/ClientPocketComputers.java
index 1905e2af4..120f9d3ea 100644
--- a/projects/common/src/client/java/dan200/computercraft/client/pocket/ClientPocketComputers.java
+++ b/projects/common/src/client/java/dan200/computercraft/client/pocket/ClientPocketComputers.java
@@ -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}.
*
* This is populated by {@link PocketComputerDataMessage} and accessed when rendering pocket computers
*/
public final class ClientPocketComputers {
- private static final Int2ObjectMap instances = new Int2ObjectOpenHashMap<>();
+ private static final Map instances = new HashMap<>();
private ClientPocketComputers() {
}
@@ -27,25 +32,32 @@ public final class ClientPocketComputers {
instances.clear();
}
- public static void remove(int id) {
+ public static void remove(UUID id) {
instances.remove(id);
}
/**
- * Get or create a pocket computer.
+ * Set the state of a pocket computer.
*
- * @param instanceId The instance ID of the pocket computer.
- * @param advanced Whether this computer has an advanced terminal.
- * @return The pocket computer data.
+ * @param instanceId The instance ID of the pocket computer.
+ * @param state The computer state of the pocket computer.
+ * @param lightColour The current colour of the modem light.
+ * @param terminalData The current terminal contents.
*/
- public static PocketComputerData get(int instanceId, boolean advanced) {
+ public static void setState(UUID instanceId, ComputerState state, int lightColour, TerminalState terminalData) {
var computer = instances.get(instanceId);
- if (computer == null) instances.put(instanceId, computer = new PocketComputerData(advanced));
- return computer;
+ if (computer == null) {
+ var terminal = new NetworkedTerminal(terminalData.width, terminalData.height, terminalData.colour);
+ instances.put(instanceId, computer = new PocketComputerData(state, lightColour, terminal));
+ } else {
+ computer.setState(state, lightColour);
+ }
+
+ if (terminalData.hasTerminal()) terminalData.apply(computer.getTerminal());
}
- public static PocketComputerData get(ItemStack stack) {
- var family = stack.getItem() instanceof PocketComputerItem computer ? computer.getFamily() : ComputerFamily.NORMAL;
- return get(PocketComputerItem.getInstanceID(stack), family != ComputerFamily.NORMAL);
+ public static @Nullable PocketComputerData get(ItemStack stack) {
+ var id = PocketComputerItem.getInstanceID(stack);
+ return id == null ? null : instances.get(id);
}
}
diff --git a/projects/common/src/client/java/dan200/computercraft/client/pocket/PocketComputerData.java b/projects/common/src/client/java/dan200/computercraft/client/pocket/PocketComputerData.java
index 03eb09a95..a5cf16d31 100644
--- a/projects/common/src/client/java/dan200/computercraft/client/pocket/PocketComputerData.java
+++ b/projects/common/src/client/java/dan200/computercraft/client/pocket/PocketComputerData.java
@@ -4,11 +4,8 @@
package dan200.computercraft.client.pocket;
-import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
-import dan200.computercraft.shared.computer.terminal.TerminalState;
-import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.pocket.core.PocketServerComputer;
/**
@@ -21,20 +18,22 @@ import dan200.computercraft.shared.pocket.core.PocketServerComputer;
* @see ClientPocketComputers The registry which holds pocket computers.
* @see PocketServerComputer The server-side pocket computer.
*/
-public class PocketComputerData {
+public final class PocketComputerData {
private final NetworkedTerminal terminal;
- private ComputerState state = ComputerState.OFF;
- private int lightColour = -1;
+ private ComputerState state;
+ private int lightColour;
- public PocketComputerData(boolean colour) {
- terminal = new NetworkedTerminal(Config.pocketTermWidth, Config.pocketTermHeight, colour);
+ PocketComputerData(ComputerState state, int lightColour, NetworkedTerminal terminal) {
+ this.state = state;
+ this.lightColour = lightColour;
+ this.terminal = terminal;
}
public int getLightState() {
return state != ComputerState.OFF ? lightColour : -1;
}
- public Terminal getTerminal() {
+ public NetworkedTerminal getTerminal() {
return terminal;
}
@@ -42,12 +41,8 @@ public class PocketComputerData {
return state;
}
- public void setState(ComputerState state, int lightColour) {
+ void setState(ComputerState state, int lightColour) {
this.state = state;
this.lightColour = lightColour;
}
-
- public void setTerminal(TerminalState state) {
- state.apply(terminal);
- }
}
diff --git a/projects/common/src/client/java/dan200/computercraft/client/render/PocketItemRenderer.java b/projects/common/src/client/java/dan200/computercraft/client/render/PocketItemRenderer.java
index f305d4e6f..676f13556 100644
--- a/projects/common/src/client/java/dan200/computercraft/client/render/PocketItemRenderer.java
+++ b/projects/common/src/client/java/dan200/computercraft/client/render/PocketItemRenderer.java
@@ -11,6 +11,7 @@ import dan200.computercraft.client.pocket.ClientPocketComputers;
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
import dan200.computercraft.core.util.Colour;
import dan200.computercraft.shared.computer.core.ComputerFamily;
+import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.world.item.ItemStack;
@@ -32,10 +33,16 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
@Override
protected void renderItem(PoseStack transform, MultiBufferSource bufferSource, ItemStack stack, int light) {
var computer = ClientPocketComputers.get(stack);
- var terminal = computer.getTerminal();
+ var terminal = computer == null ? null : computer.getTerminal();
- var termWidth = terminal.getWidth();
- var termHeight = terminal.getHeight();
+ int termWidth, termHeight;
+ if (terminal == null) {
+ termWidth = Config.pocketTermWidth;
+ termHeight = Config.pocketTermHeight;
+ } else {
+ termWidth = terminal.getWidth();
+ termHeight = terminal.getHeight();
+ }
var width = termWidth * FONT_WIDTH + MARGIN * 2;
var height = termHeight * FONT_HEIGHT + MARGIN * 2;
@@ -60,14 +67,15 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
renderFrame(matrix, bufferSource, family, frameColour, light, width, height);
// Render the light
- var lightColour = ClientPocketComputers.get(stack).getLightState();
- if (lightColour == -1) lightColour = Colour.BLACK.getHex();
+ var lightColour = computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState();
renderLight(transform, bufferSource, lightColour, width, height);
- FixedWidthFontRenderer.drawTerminal(
- FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL)),
- MARGIN, MARGIN, terminal, MARGIN, MARGIN, MARGIN, MARGIN
- );
+ var quadEmitter = FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL));
+ if (terminal == null) {
+ FixedWidthFontRenderer.drawEmptyTerminal(quadEmitter, 0, 0, width, height);
+ } else {
+ FixedWidthFontRenderer.drawTerminal(quadEmitter, MARGIN, MARGIN, terminal, MARGIN, MARGIN, MARGIN, MARGIN);
+ }
transform.popPose();
}
diff --git a/projects/common/src/client/java/dan200/computercraft/client/render/monitor/MonitorBlockEntityRenderer.java b/projects/common/src/client/java/dan200/computercraft/client/render/monitor/MonitorBlockEntityRenderer.java
index 98b1c37d2..055f6d68b 100644
--- a/projects/common/src/client/java/dan200/computercraft/client/render/monitor/MonitorBlockEntityRenderer.java
+++ b/projects/common/src/client/java/dan200/computercraft/client/render/monitor/MonitorBlockEntityRenderer.java
@@ -60,9 +60,9 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer 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);
}
diff --git a/projects/common/src/main/java/dan200/computercraft/impl/network/wired/InvariantChecker.java b/projects/common/src/main/java/dan200/computercraft/impl/network/wired/InvariantChecker.java
index ea69bbbc4..0e627f7dc 100644
--- a/projects/common/src/main/java/dan200/computercraft/impl/network/wired/InvariantChecker.java
+++ b/projects/common/src/main/java/dan200/computercraft/impl/network/wired/InvariantChecker.java
@@ -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.
*
- * 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 @Nullable T makeNullable(T object) {
+ return object;
}
}
diff --git a/projects/common/src/main/java/dan200/computercraft/impl/network/wired/NodeSet.java b/projects/common/src/main/java/dan200/computercraft/impl/network/wired/NodeSet.java
new file mode 100644
index 000000000..9d3f0808e
--- /dev/null
+++ b/projects/common/src/main/java/dan200/computercraft/impl/network/wired/NodeSet.java
@@ -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.
+ *
+ * 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 Disjoint-set data structure
+ */
+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);
+ }
+}
diff --git a/projects/common/src/main/java/dan200/computercraft/impl/network/wired/WiredNetworkChangeImpl.java b/projects/common/src/main/java/dan200/computercraft/impl/network/wired/WiredNetworkChangeImpl.java
index 007b660c1..c302f3b1b 100644
--- a/projects/common/src/main/java/dan200/computercraft/impl/network/wired/WiredNetworkChangeImpl.java
+++ b/projects/common/src/main/java/dan200/computercraft/impl/network/wired/WiredNetworkChangeImpl.java
@@ -6,6 +6,7 @@ package dan200.computercraft.impl.network.wired;
import dan200.computercraft.api.network.wired.WiredNetworkChange;
import dan200.computercraft.api.peripheral.IPeripheral;
+import dan200.computercraft.core.util.PeripheralHelpers;
import java.util.Collections;
import java.util.HashMap;
@@ -52,7 +53,7 @@ final class WiredNetworkChangeImpl implements WiredNetworkChange {
var oldValue = entry.getValue();
if (newPeripherals.containsKey(oldKey)) {
var rightValue = added.get(oldKey);
- if (oldValue.equals(rightValue)) {
+ if (PeripheralHelpers.equals(oldValue, rightValue)) {
added.remove(oldKey);
} else {
removed.put(oldKey, oldValue);
diff --git a/projects/common/src/main/java/dan200/computercraft/impl/network/wired/WiredNetworkImpl.java b/projects/common/src/main/java/dan200/computercraft/impl/network/wired/WiredNetworkImpl.java
index ef8527304..c297b7294 100644
--- a/projects/common/src/main/java/dan200/computercraft/impl/network/wired/WiredNetworkImpl.java
+++ b/projects/common/src/main/java/dan200/computercraft/impl/network/wired/WiredNetworkImpl.java
@@ -8,6 +8,7 @@ import dan200.computercraft.api.network.Packet;
import dan200.computercraft.api.network.wired.WiredNetwork;
import dan200.computercraft.api.network.wired.WiredNode;
import dan200.computercraft.api.peripheral.IPeripheral;
+import dan200.computercraft.core.util.Nullability;
import java.util.*;
import java.util.concurrent.locks.ReadWriteLock;
@@ -187,10 +188,76 @@ final class WiredNetworkImpl implements WiredNetwork {
return true;
}
- var reachable = reachableNodes(neighbours.iterator().next());
+ assert neighbours.size() >= 2 : "Must have more than one neighbour.";
+
+ /*
+ Otherwise we need to find all sets of connected nodes within the graph, and split them off into their own
+ networks.
+
+ With our current graph representation[^1], this requires a traversal of the graph, taking O(|V| + |E))
+ time, which can get quite expensive for large graphs. We try to avoid this traversal where possible, by
+ optimising for the case where the graph remains fully connected after removing this node, for instance,
+ removing "A" here:
+
+ A---B B
+ | | => |
+ C---D C---D
+
+ We observe that these sorts of loops tend to be local, and so try to identify them as quickly as possible.
+ To do this, we do a standard breadth-first traversal of the graph starting at the neighbours of the
+ removed node, building sets of connected nodes.
+
+ If, at any point, all nodes visited so far are connected to each other, then we know all remaining nodes
+ will also be connected. This allows us to abort our traversal of the graph, and just remove the node (much
+ like we do in the single neighbour case above).
+
+ Otherwise, we then just create a new network for each disjoint set of connected nodes.
+
+ {^1]:
+ There are efficient (near-logarithmic) algorithms for this (e.g. https://arxiv.org/pdf/1609.05867.pdf),
+ but they are significantly more complex to implement.
+ */
+
+ // Create a new set of nodes for each neighbour, and add them to our queue of nodes to visit.
+ List queue = new ArrayList<>();
+ Set nodeSets = new HashSet<>(neighbours.size());
+ for (var neighbour : neighbours) {
+ nodeSets.add(neighbour.currentSet = new NodeSet());
+ queue.add(neighbour);
+ }
+
+ // Perform a breadth-first search of the graph, starting from the neighbours.
+ graphSearch:
+ for (var i = 0; i < queue.size(); i++) {
+ var enqueuedNode = queue.get(i);
+ for (var neighbour : enqueuedNode.neighbours) {
+ var nodeSet = Nullability.assertNonNull(enqueuedNode.currentSet).find();
+
+ // The neighbour has no set and so has not been visited yet. Add it to the current set and enqueue
+ // it to be visited.
+ if (neighbour.currentSet == null) {
+ nodeSet.addNode(neighbour);
+ queue.add(neighbour);
+ continue;
+ }
+
+ // Otherwise, take the union of the two nodes' sets if needed. If we've only got a single node set
+ // left, then we know the whole graph is network is connected (even if not all nodes have been
+ // visited) and so can abort early.
+ var neighbourSet = neighbour.currentSet.find();
+ if (nodeSet != neighbourSet) {
+ var removed = nodeSets.remove(NodeSet.merge(nodeSet, neighbourSet));
+ assert removed : "Merged set should have been ";
+ if (nodeSets.size() == 1) break graphSearch;
+ }
+ }
+ }
+
+ // If we have a single subset, then all nodes are reachable - just clear the set and exit.
+ if (nodeSets.size() == 1) {
+ assert nodeSets.iterator().next().size() == queue.size();
+ for (var neighbour : queue) neighbour.currentSet = null;
- // If all nodes are reachable then exit.
- if (reachable.size() == nodes.size()) {
// Broadcast our simple peripheral changes
removeSingleNode(wired, wiredNetwork);
InvariantChecker.checkNode(wired);
@@ -198,43 +265,46 @@ final class WiredNetworkImpl implements WiredNetwork {
return true;
}
- // A split may cause 2..neighbours.size() separate networks, so we
- // iterate through our neighbour list, generating child networks.
- neighbours.removeAll(reachable);
- var maximals = new ArrayList(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(1 + nodeSets.size());
+ // Add the network we've created for the removed node.
+ networks.add(wiredNetwork);
+ // And then create a new network for each disjoint subset.
+ for (var set : nodeSets) {
+ var network = new WiredNetworkImpl(new HashSet<>(set.size()));
+ set.setNetwork(network);
+ networks.add(network);
}
- for (var network : maximals) network.lock.writeLock().lock();
+ for (var network : networks) network.lock.writeLock().lock();
try {
// We special case the original node: detaching all peripherals when needed.
wired.network = wiredNetwork;
wired.peripherals = Map.of();
+ wired.neighbours.clear();
- // Ensure every network is finalised
- for (var network : maximals) {
- for (var child : network.nodes) {
- child.network = network;
- network.peripherals.putAll(child.peripherals);
- }
+ // Add all nodes to their appropriate network.
+ for (var child : queue) {
+ var network = Nullability.assertNonNull(child.currentSet).network();
+ child.currentSet = null;
+
+ child.network = network;
+ network.nodes.add(child);
+ network.peripherals.putAll(child.peripherals);
}
- for (var network : maximals) InvariantChecker.checkNetwork(network);
+ for (var network : networks) InvariantChecker.checkNetwork(network);
InvariantChecker.checkNode(wired);
// Then broadcast network changes once all nodes are finalised
- for (var network : maximals) {
+ for (var network : networks) {
WiredNetworkChangeImpl.changeOf(peripherals, network.peripherals).broadcast(network.nodes);
}
} finally {
- for (var network : maximals) network.lock.writeLock().unlock();
+ for (var network : networks) network.lock.writeLock().unlock();
}
nodes.clear();
@@ -373,22 +443,4 @@ final class WiredNetworkImpl implements WiredNetwork {
throw new IllegalArgumentException("Unknown implementation of IWiredNode: " + node);
}
}
-
- private static Set reachableNodes(WiredNodeImpl start) {
- Queue enqueued = new ArrayDeque<>();
- var reachable = new HashSet();
-
- 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;
- }
}
diff --git a/projects/common/src/main/java/dan200/computercraft/impl/network/wired/WiredNodeImpl.java b/projects/common/src/main/java/dan200/computercraft/impl/network/wired/WiredNodeImpl.java
index d402053cf..ec9d6015e 100644
--- a/projects/common/src/main/java/dan200/computercraft/impl/network/wired/WiredNodeImpl.java
+++ b/projects/common/src/main/java/dan200/computercraft/impl/network/wired/WiredNodeImpl.java
@@ -27,11 +27,39 @@ public final class WiredNodeImpl implements WiredNode {
final HashSet 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 peripherals) {
+ network.updatePeripherals(this, peripherals);
+ }
+
@Override
public synchronized void addReceiver(PacketReceiver receiver) {
if (receivers == null) receivers = new HashSet<>();
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/CommonHooks.java b/projects/common/src/main/java/dan200/computercraft/shared/CommonHooks.java
index 8e1e96c59..5350075f1 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/CommonHooks.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/CommonHooks.java
@@ -18,6 +18,7 @@ import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.dedicated.DedicatedServer;
+import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.packs.resources.PreparableReloadListener;
import net.minecraft.world.entity.Entity;
@@ -78,10 +79,19 @@ public final class CommonHooks {
NetworkUtils.reset();
}
+ public static void onServerChunkUnload(LevelChunk chunk) {
+ if (!(chunk.getLevel() instanceof ServerLevel)) throw new IllegalArgumentException("Not a server chunk.");
+ TickScheduler.onChunkUnload(chunk);
+ }
+
public static void onChunkWatch(LevelChunk chunk, ServerPlayer player) {
MonitorWatcher.onWatch(chunk, player);
}
+ public static void onChunkTicketLevelChanged(ServerLevel level, long chunkPos, int oldLevel, int newLevel) {
+ TickScheduler.onChunkTicketChanged(level, chunkPos, oldLevel, newLevel);
+ }
+
public static final ResourceLocation TREASURE_DISK_LOOT = new ResourceLocation(ComputerCraftAPI.MOD_ID, "treasure_disk");
private static final Set TREASURE_DISK_LOOT_TABLES = Set.of(
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java b/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java
index 3ca2e73e0..22a109d61 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java
@@ -19,7 +19,6 @@ import dan200.computercraft.impl.PocketUpgrades;
import dan200.computercraft.impl.TurtleUpgrades;
import dan200.computercraft.shared.command.UserLevel;
import dan200.computercraft.shared.command.arguments.ComputerArgumentType;
-import dan200.computercraft.shared.command.arguments.ComputersArgumentType;
import dan200.computercraft.shared.command.arguments.RepeatArgumentType;
import dan200.computercraft.shared.command.arguments.TrackingFieldArgumentType;
import dan200.computercraft.shared.common.ClearColourRecipe;
@@ -338,8 +337,7 @@ public final class ModRegistry {
static {
register("tracking_field", TrackingFieldArgumentType.class, TrackingFieldArgumentType.metric());
- register("computer", ComputerArgumentType.class, ComputerArgumentType.oneComputer());
- register("computers", ComputersArgumentType.class, new ComputersArgumentType.Info());
+ register("computer", ComputerArgumentType.class, ComputerArgumentType.get());
registerUnsafe("repeat", RepeatArgumentType.class, new RepeatArgumentType.Info());
}
}
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java b/projects/common/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java
index f67d07ced..de1d5a5f3 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java
@@ -12,7 +12,8 @@ import com.mojang.brigadier.suggestion.Suggestions;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.shared.ModRegistry;
-import dan200.computercraft.shared.command.arguments.ComputersArgumentType;
+import dan200.computercraft.shared.command.arguments.ComputerArgumentType;
+import dan200.computercraft.shared.command.arguments.ComputerSelector;
import dan200.computercraft.shared.command.text.TableBuilder;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
@@ -23,6 +24,7 @@ import dan200.computercraft.shared.computer.metrics.basic.AggregatedMetric;
import dan200.computercraft.shared.computer.metrics.basic.BasicComputerMetricsObserver;
import dan200.computercraft.shared.computer.metrics.basic.ComputerMetrics;
import dan200.computercraft.shared.network.container.ComputerContainerData;
+import net.minecraft.ChatFormatting;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
@@ -42,9 +44,6 @@ import java.util.*;
import static dan200.computercraft.shared.command.CommandUtils.isPlayer;
import static dan200.computercraft.shared.command.Exceptions.NOT_TRACKING_EXCEPTION;
import static dan200.computercraft.shared.command.Exceptions.NO_TIMINGS_EXCEPTION;
-import static dan200.computercraft.shared.command.arguments.ComputerArgumentType.getComputerArgument;
-import static dan200.computercraft.shared.command.arguments.ComputerArgumentType.oneComputer;
-import static dan200.computercraft.shared.command.arguments.ComputersArgumentType.*;
import static dan200.computercraft.shared.command.arguments.TrackingFieldArgumentType.metric;
import static dan200.computercraft.shared.command.builder.CommandBuilder.args;
import static dan200.computercraft.shared.command.builder.CommandBuilder.command;
@@ -70,37 +69,37 @@ public final class CommandComputerCraft {
.requires(ModRegistry.Permissions.PERMISSION_DUMP)
.executes(c -> dump(c.getSource()))
.then(args()
- .arg("computer", oneComputer())
- .executes(c -> dumpComputer(c.getSource(), getComputerArgument(c, "computer")))))
+ .arg("computer", ComputerArgumentType.get())
+ .executes(c -> dumpComputer(c.getSource(), ComputerArgumentType.getOne(c, "computer")))))
.then(command("shutdown")
.requires(ModRegistry.Permissions.PERMISSION_SHUTDOWN)
- .argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers())
+ .argManyValue("computers", ComputerArgumentType.get(), ComputerSelector.all())
.executes((c, a) -> shutdown(c.getSource(), unwrap(c.getSource(), a))))
.then(command("turn-on")
.requires(ModRegistry.Permissions.PERMISSION_TURN_ON)
- .argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers())
+ .argManyValue("computers", ComputerArgumentType.get(), ComputerSelector.all())
.executes((c, a) -> turnOn(c.getSource(), unwrap(c.getSource(), a))))
.then(command("tp")
.requires(ModRegistry.Permissions.PERMISSION_TP)
- .arg("computer", oneComputer())
- .executes(c -> teleport(c.getSource(), getComputerArgument(c, "computer"))))
+ .arg("computer", ComputerArgumentType.get())
+ .executes(c -> teleport(c.getSource(), ComputerArgumentType.getOne(c, "computer"))))
.then(command("queue")
.requires(ModRegistry.Permissions.PERMISSION_QUEUE)
.arg(
- RequiredArgumentBuilder.argument("computer", manyComputers())
+ RequiredArgumentBuilder.argument("computer", ComputerArgumentType.get())
.suggests((context, builder) -> Suggestions.empty())
)
.argManyValue("args", StringArgumentType.string(), List.of())
- .executes((c, a) -> queue(getComputersArgument(c, "computer"), a)))
+ .executes((c, a) -> queue(ComputerArgumentType.getMany(c, "computer"), a)))
.then(command("view")
.requires(ModRegistry.Permissions.PERMISSION_VIEW)
- .arg("computer", oneComputer())
- .executes(c -> view(c.getSource(), getComputerArgument(c, "computer"))))
+ .arg("computer", ComputerArgumentType.get())
+ .executes(c -> view(c.getSource(), ComputerArgumentType.getOne(c, "computer"))))
.then(choice("track")
.requires(ModRegistry.Permissions.PERMISSION_TRACK)
@@ -135,7 +134,7 @@ public final class CommandComputerCraft {
} else if (b.getLevel() == world) {
return 1;
} else {
- return Integer.compare(a.getInstanceID(), b.getInstanceID());
+ return a.getInstanceUUID().compareTo(b.getInstanceUUID());
}
});
@@ -160,7 +159,8 @@ public final class CommandComputerCraft {
*/
private static int dumpComputer(CommandSourceStack source, ServerComputer computer) {
var table = new TableBuilder("Dump");
- table.row(header("Instance"), text(Integer.toString(computer.getInstanceID())));
+ table.row(header("Instance ID"), text(Integer.toString(computer.getInstanceID())));
+ table.row(header("Instance UUID"), text(computer.getInstanceUUID().toString()));
table.row(header("Id"), text(Integer.toString(computer.getID())));
table.row(header("Label"), text(computer.getLabel()));
table.row(header("On"), bool(computer.isOn()));
@@ -332,29 +332,22 @@ public final class CommandComputerCraft {
// Additional helper functions.
- private static Component linkComputer(CommandSourceStack source, @Nullable ServerComputer serverComputer, int computerId) {
+ private static Component linkComputer(CommandSourceStack source, @Nullable ServerComputer computer, int computerId) {
var out = Component.literal("");
- // Append the computer instance
- if (serverComputer == null) {
- out.append(text("?"));
+ // And instance
+ if (computer == null) {
+ out.append("#" + computerId + " ").append(coloured("(unloaded)", ChatFormatting.GRAY));
} else {
- out.append(link(
- text(Integer.toString(serverComputer.getInstanceID())),
- "/computercraft dump " + serverComputer.getInstanceID(),
- Component.translatable("commands.computercraft.dump.action")
- ));
+ out.append(makeComputerDumpCommand(computer));
}
- // And ID
- out.append(" (id " + computerId + ")");
-
// And, if we're a player, some useful links
- if (serverComputer != null && isPlayer(source)) {
+ if (computer != null && isPlayer(source)) {
if (ModRegistry.Permissions.PERMISSION_TP.test(source)) {
out.append(" ").append(link(
text("\u261b"),
- "/computercraft tp " + serverComputer.getInstanceID(),
+ makeComputerCommand("tp", computer),
Component.translatable("commands.computercraft.tp.action")
));
}
@@ -362,7 +355,7 @@ public final class CommandComputerCraft {
if (ModRegistry.Permissions.PERMISSION_VIEW.test(source)) {
out.append(" ").append(link(
text("\u20e2"),
- "/computercraft view " + serverComputer.getInstanceID(),
+ makeComputerCommand("view", computer),
Component.translatable("commands.computercraft.view.action")
));
}
@@ -380,7 +373,7 @@ public final class CommandComputerCraft {
if (ModRegistry.Permissions.PERMISSION_TP.test(context)) {
return link(
position(computer.getPosition()),
- "/computercraft tp " + computer.getInstanceID(),
+ makeComputerCommand("tp", computer),
Component.translatable("commands.computercraft.tp.action")
);
} else {
@@ -392,7 +385,7 @@ public final class CommandComputerCraft {
var file = new File(ServerContext.get(source.getServer()).storageDir().toFile(), "computer/" + id);
if (!file.isDirectory()) return null;
- return link(
+ return clientLink(
text("\u270E"),
"/" + CLIENT_OPEN_FOLDER + " " + id,
Component.translatable("commands.computercraft.dump.open_path")
@@ -431,4 +424,10 @@ public final class CommandComputerCraft {
table.display(source);
return timings.size();
}
+
+ public static Set unwrap(CommandSourceStack source, Collection suppliers) {
+ Set computers = new HashSet<>();
+ for (var supplier : suppliers) supplier.find(source).forEach(computers::add);
+ return computers;
+ }
}
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/CommandUtils.java b/projects/common/src/main/java/dan200/computercraft/shared/command/CommandUtils.java
index 57d935dab..dd2f4ad58 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/command/CommandUtils.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/command/CommandUtils.java
@@ -28,12 +28,12 @@ public final class CommandUtils {
@SuppressWarnings("unchecked")
public static CompletableFuture suggestOnServer(CommandContext> context, Function, CompletableFuture> 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) context);
} else {
- return ((SharedSuggestionProvider) source).customSuggestion(context);
+ return shared.customSuggestion(context);
}
}
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/Exceptions.java b/projects/common/src/main/java/dan200/computercraft/shared/command/Exceptions.java
index 73ea11a1b..f90951dac 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/command/Exceptions.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/command/Exceptions.java
@@ -7,6 +7,8 @@ package dan200.computercraft.shared.command;
import com.mojang.brigadier.exceptions.Dynamic2CommandExceptionType;
import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
+import net.minecraft.commands.arguments.selector.EntitySelectorParser;
+import net.minecraft.commands.arguments.selector.options.EntitySelectorOptions;
import net.minecraft.network.chat.Component;
public final class Exceptions {
@@ -20,6 +22,13 @@ public final class Exceptions {
public static final SimpleCommandExceptionType ARGUMENT_EXPECTED = translated("argument.computercraft.argument_expected");
+ public static final DynamicCommandExceptionType UNKNOWN_FAMILY = translated1("argument.computercraft.unknown_computer_family");
+
+ public static final DynamicCommandExceptionType ERROR_EXPECTED_OPTION_VALUE = EntitySelectorParser.ERROR_EXPECTED_OPTION_VALUE;
+ public static final SimpleCommandExceptionType ERROR_EXPECTED_END_OF_OPTIONS = EntitySelectorParser.ERROR_EXPECTED_END_OF_OPTIONS;
+ public static final DynamicCommandExceptionType ERROR_UNKNOWN_OPTION = EntitySelectorOptions.ERROR_UNKNOWN_OPTION;
+ public static final DynamicCommandExceptionType ERROR_INAPPLICABLE_OPTION = EntitySelectorOptions.ERROR_INAPPLICABLE_OPTION;
+
private static SimpleCommandExceptionType translated(String key) {
return new SimpleCommandExceptionType(Component.translatable(key));
}
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/arguments/ArgumentParserUtils.java b/projects/common/src/main/java/dan200/computercraft/shared/command/arguments/ArgumentParserUtils.java
new file mode 100644
index 000000000..775a56e8d
--- /dev/null
+++ b/projects/common/src/main/java/dan200/computercraft/shared/command/arguments/ArgumentParserUtils.java
@@ -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;
+ }
+}
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/arguments/ComputerArgumentType.java b/projects/common/src/main/java/dan200/computercraft/shared/command/arguments/ComputerArgumentType.java
index 5e8a4f03d..5a645dd58 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/command/arguments/ComputerArgumentType.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/command/arguments/ComputerArgumentType.java
@@ -14,66 +14,58 @@ import dan200.computercraft.shared.computer.core.ServerComputer;
import net.minecraft.commands.CommandSourceStack;
import java.util.Collection;
+import java.util.List;
import java.util.concurrent.CompletableFuture;
-import static dan200.computercraft.shared.command.Exceptions.COMPUTER_ARG_MANY;
-
-public final class ComputerArgumentType implements ArgumentType {
+public final class ComputerArgumentType implements ArgumentType {
private static final ComputerArgumentType INSTANCE = new ComputerArgumentType();
- public static ComputerArgumentType oneComputer() {
- return INSTANCE;
- }
+ private static final List EXAMPLES = List.of(
+ "0", "123", "@c[instance_id=123]"
+ );
- public static ServerComputer getComputerArgument(CommandContext 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 getMany(CommandContext 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 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 CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) {
- return ComputersArgumentType.someComputers().listSuggestions(context, builder);
+ return ComputerSelector.suggest(context, builder);
}
@Override
public Collection getExamples() {
- return ComputersArgumentType.someComputers().getExamples();
- }
-
- @FunctionalInterface
- public interface ComputerSupplier {
- ServerComputer unwrap(CommandSourceStack source) throws CommandSyntaxException;
+ return EXAMPLES;
}
}
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/arguments/ComputerSelector.java b/projects/common/src/main/java/dan200/computercraft/shared/command/arguments/ComputerSelector.java
new file mode 100644
index 000000000..0b013906f
--- /dev/null
+++ b/projects/common/src/main/java/dan200/computercraft/shared/command/arguments/ComputerSelector.java
@@ -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 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 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 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 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 suggestSelector(CommandContext> context, StringReader reader) {
+ Set 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 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 options;
+
+ /**
+ * Get a map of individual selector options.
+ *
+ * @return The available options.
+ */
+ public static Map 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 suggest(CommandContext> source, SuggestionsBuilder builder);
+ }
+
+ private static SuggestionProvider suggestComputers(Function 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();
+ });
+ }
+}
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/arguments/ComputersArgumentType.java b/projects/common/src/main/java/dan200/computercraft/shared/command/arguments/ComputersArgumentType.java
deleted file mode 100644
index 7cbee3b39..000000000
--- a/projects/common/src/main/java/dan200/computercraft/shared/command/arguments/ComputersArgumentType.java
+++ /dev/null
@@ -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 {
- private static final ComputersArgumentType MANY = new ComputersArgumentType(false);
- private static final ComputersArgumentType SOME = new ComputersArgumentType(true);
-
- private static final List EXAMPLES = List.of(
- "0", "#0", "@Label", "~Advanced"
- );
-
- public static ComputersArgumentType manyComputers() {
- return MANY;
- }
-
- public static ComputersArgumentType someComputers() {
- return SOME;
- }
-
- public static Collection getComputersArgument(CommandContext 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 CompletableFuture listSuggestions(CommandContext 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 getExamples() {
- return EXAMPLES;
- }
-
- private static void suggestComputers(CommandSourceStack source, SuggestionsBuilder builder, String remaining, Function 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 predicate) {
- return s -> ServerContext.get(s.getServer()).registry()
- .getComputers()
- .stream()
- .filter(predicate)
- .toList();
- }
-
- public static class Info implements ArgumentTypeInfo {
- @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 {
- @Override
- public ComputersArgumentType instantiate(CommandBuildContext context) {
- return requireSome ? SOME : MANY;
- }
-
- @Override
- public Info type() {
- return info;
- }
- }
-
- @FunctionalInterface
- public interface ComputersSupplier {
- Collection unwrap(CommandSourceStack source) throws CommandSyntaxException;
- }
-
- public static Set unwrap(CommandSourceStack source, Collection suppliers) throws CommandSyntaxException {
- Set computers = new HashSet<>();
- for (var supplier : suppliers) computers.addAll(supplier.unwrap(source));
- return computers;
- }
-}
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/text/ChatHelpers.java b/projects/common/src/main/java/dan200/computercraft/shared/command/text/ChatHelpers.java
index bf3479c5f..849654875 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/command/text/ChatHelpers.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/command/text/ChatHelpers.java
@@ -4,6 +4,8 @@
package dan200.computercraft.shared.command.text;
+import dan200.computercraft.shared.computer.core.ServerComputer;
+import dan200.computercraft.shared.platform.PlatformHelper;
import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.ClickEvent;
@@ -53,6 +55,13 @@ public final class ChatHelpers {
return link(component, new ClickEvent(ClickEvent.Action.RUN_COMMAND, command), toolTip);
}
+ public static Component clientLink(MutableComponent component, String command, Component toolTip) {
+ var event = PlatformHelper.get().canClickRunClientCommand()
+ ? new ClickEvent(ClickEvent.Action.RUN_COMMAND, command)
+ : new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, command);
+ return link(component, event, toolTip);
+ }
+
public static Component link(Component component, ClickEvent click, Component toolTip) {
var style = component.getStyle();
@@ -73,4 +82,16 @@ public final class ChatHelpers {
.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.translatable("gui.computercraft.tooltip.copy")))
);
}
+
+ public static String makeComputerCommand(String command, ServerComputer computer) {
+ return String.format("/computercraft %s @c[instance=%s]", command, computer.getInstanceUUID());
+ }
+
+ public static Component makeComputerDumpCommand(ServerComputer computer) {
+ return link(
+ text("#" + computer.getID()),
+ makeComputerCommand("dump", computer),
+ Component.translatable("commands.computercraft.dump.action")
+ );
+ }
}
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/AbstractComputerBlock.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/AbstractComputerBlock.java
index 15ea4e26f..9071ebe39 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/AbstractComputerBlock.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/AbstractComputerBlock.java
@@ -23,6 +23,7 @@ import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
+import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.EntityBlock;
@@ -176,6 +177,15 @@ public abstract class AbstractComputerBlock
+ * This is called immediately when a neighbouring block changes (see {@link #neighborChanged(BlockPos)}).
+ *
+ * @param computer The current server computer.
+ * @param dir The direction to update in.
+ * @param targetPos The position of the adjacent block, equal to {@code getBlockPos().offset(dir)}.
+ */
private void updateRedstoneInput(ServerComputer computer, Direction dir, BlockPos targetPos) {
var offsetSide = dir.getOpposite();
var localDir = remapToLocalSide(dir);
@@ -211,6 +227,15 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
computer.setBundledRedstoneInput(localDir, BundledRedstone.getOutput(getLevel(), targetPos, offsetSide));
}
+ /**
+ * Update the peripheral on a particular side.
+ *
+ * This is called from {@link #serverTick()}, after a peripheral has been marked as invalid (such as in
+ * {@link #neighborChanged(BlockPos)})
+ *
+ * @param computer The current server computer.
+ * @param dir The direction to update in.
+ */
private void refreshPeripheral(ServerComputer computer, Direction dir) {
invalidSides &= ~(1 << dir.ordinal());
@@ -243,7 +268,18 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
}
}
- private void updateInputAt(BlockPos neighbour) {
+ /**
+ * Called when a neighbour block changes.
+ *
+ * This finds the side the neighbour block is on, and updates the inputs accordingly.
+ *
+ * We do NOT update the peripheral immediately. Blocks and block entities are sometimes
+ * inconsistent at the point where an update is received, and so we instead just mark that side as dirty (see
+ * {@link #invalidSides}) and refresh it {@linkplain #serverTick() next tick}.
+ *
+ * @param neighbour The position of the neighbour block.
+ */
+ public void neighborChanged(BlockPos neighbour) {
var computer = getServerComputer();
if (computer == null) return;
@@ -258,22 +294,39 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
// If the position is not any adjacent one, update all inputs. This is pretty terrible, but some redstone mods
// handle this incorrectly.
- updateRedstoneInputs(computer);
- invalidSides = (1 << 6) - 1; // Mark all peripherals as dirty.
+ for (var dir : DirectionUtil.FACINGS) updateRedstoneInput(computer, dir, getBlockPos().relative(dir));
+ invalidSides = DirectionUtil.ALL_SIDES; // Mark all peripherals as dirty.
}
/**
- * Update the block's state and propagate redstone output.
+ * Called when a neighbour block's shape changes.
+ *
+ * Unlike {@link #neighborChanged(BlockPos)}, we don't update redstone, only peripherals.
+ *
+ * @param direction The side that changed.
*/
- public void updateOutput() {
- BlockEntityHelpers.updateBlock(this);
- for (var dir : DirectionUtil.FACINGS) RedstoneUtil.propagateRedstoneOutput(getLevel(), getBlockPos(), dir);
-
- var computer = getServerComputer();
- if (computer != null) updateRedstoneInputs(computer);
+ public void neighbourShapeChanged(Direction direction) {
+ invalidSides |= 1 << direction.ordinal();
}
- protected abstract ServerComputer createComputer(int id);
+ /**
+ * Update outputs in a specific direction.
+ *
+ * @param direction The direction to propagate outputs in.
+ */
+ protected void updateRedstoneTo(Direction direction) {
+ RedstoneUtil.propagateRedstoneOutput(getLevel(), getBlockPos(), direction);
+
+ var computer = getServerComputer();
+ if (computer != null) updateRedstoneInput(computer, direction, getBlockPos().relative(direction));
+ }
+
+ /**
+ * Update all redstone outputs.
+ */
+ public void updateRedstone() {
+ for (var dir : DirectionUtil.FACINGS) updateRedstoneTo(dir);
+ }
@Override
public final int getComputerID() {
@@ -331,6 +384,8 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
return computer;
}
+ protected abstract ServerComputer createComputer(int id);
+
@Nullable
public ServerComputer getServerComputer() {
return getLevel().isClientSide || getLevel().getServer() == null ? null : ServerContext.get(getLevel().getServer()).registry().get(instanceID);
@@ -358,7 +413,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
}
protected void transferStateFrom(AbstractComputerBlockEntity copy) {
- if (copy.computerID != computerID || copy.instanceID != instanceID) {
+ if (copy.computerID != computerID || !Objects.equals(copy.instanceID, instanceID)) {
unload();
instanceID = copy.instanceID;
computerID = copy.computerID;
@@ -368,7 +423,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
lockCode = copy.lockCode;
BlockEntityHelpers.updateBlock(this);
}
- copy.instanceID = -1;
+ copy.instanceID = null;
}
@Override
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/ComputerBlockEntity.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/ComputerBlockEntity.java
index 6aee79628..89da54571 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/ComputerBlockEntity.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/ComputerBlockEntity.java
@@ -51,7 +51,7 @@ public class ComputerBlockEntity extends AbstractComputerBlockEntity {
protected void updateBlockState(ComputerState newState) {
var existing = getBlockState();
if (existing.getValue(ComputerBlock.STATE) != newState) {
- getLevel().setBlock(getBlockPos(), existing.setValue(ComputerBlock.STATE, newState), 3);
+ getLevel().setBlock(getBlockPos(), existing.setValue(ComputerBlock.STATE, newState), ComputerBlock.UPDATE_CLIENTS);
}
}
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java
index 72bb03570..3b9a2e6e3 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java
@@ -26,11 +26,13 @@ import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.inventory.AbstractContainerMenu;
import javax.annotation.Nullable;
+import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
public class ServerComputer implements InputHandler, ComputerEnvironment {
private final int instanceID;
+ private final UUID instanceUUID = UUID.randomUUID();
private ServerLevel level;
private BlockPos position;
@@ -42,7 +44,6 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
private final NetworkedTerminal terminal;
private final AtomicBoolean terminalChanged = new AtomicBoolean(false);
- private boolean changedLastFrame;
private int ticksSincePing;
public ServerComputer(
@@ -96,10 +97,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
public void tickServer() {
ticksSincePing++;
-
computer.tick();
-
- changedLastFrame = computer.pollAndResetChanged();
if (terminalChanged.getAndSet(false)) onTerminalChanged();
}
@@ -119,13 +117,13 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
return ticksSincePing > 100;
}
- public boolean hasOutputChanged() {
- return changedLastFrame;
+ public int pollAndResetChanges() {
+ return computer.pollAndResetChanges();
}
- public int register() {
- ServerContext.get(level.getServer()).registry().add(instanceID, this);
- return instanceID;
+ public UUID register() {
+ ServerContext.get(level.getServer()).registry().add(this);
+ return instanceUUID;
}
void unload() {
@@ -134,7 +132,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
public void close() {
unload();
- ServerContext.get(level.getServer()).registry().remove(instanceID);
+ ServerContext.get(level.getServer()).registry().remove(this);
}
private void sendToAllInteracting(Function> createPacket) {
@@ -154,6 +152,10 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
return instanceID;
}
+ public UUID getInstanceUUID() {
+ return instanceUUID;
+ }
+
public int getID() {
return computer.getID();
}
@@ -167,7 +169,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
}
public ComputerState getState() {
- if (!isOn()) return ComputerState.OFF;
+ if (!computer.isOn()) return ComputerState.OFF;
return computer.isBlinking() ? ComputerState.BLINKING : ComputerState.ON;
}
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputerRegistry.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputerRegistry.java
index 1c53aaabb..34f76b19d 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputerRegistry.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputerRegistry.java
@@ -8,14 +8,14 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import javax.annotation.Nullable;
-import java.util.Collection;
-import java.util.Random;
+import java.util.*;
public class ServerComputerRegistry {
private static final Random RANDOM = new Random();
private final int sessionId = RANDOM.nextInt();
- private final Int2ObjectMap computers = new Int2ObjectOpenHashMap<>();
+ private final Int2ObjectMap computersByInstanceId = new Int2ObjectOpenHashMap<>();
+ private final Map computersByInstanceUuid = new HashMap<>();
private int nextInstanceId;
public int getSessionID() {
@@ -28,11 +28,16 @@ public class ServerComputerRegistry {
@Nullable
public ServerComputer get(int instanceID) {
- return instanceID >= 0 ? computers.get(instanceID) : null;
+ return instanceID >= 0 ? computersByInstanceId.get(instanceID) : null;
}
@Nullable
- public ServerComputer get(int sessionId, int instanceId) {
+ public ServerComputer get(@Nullable UUID instanceID) {
+ return instanceID != null ? computersByInstanceUuid.get(instanceID) : null;
+ }
+
+ @Nullable
+ public ServerComputer get(int sessionId, @Nullable UUID instanceId) {
return sessionId == this.sessionId ? get(instanceId) : null;
}
@@ -50,28 +55,36 @@ public class ServerComputerRegistry {
}
}
- void add(int instanceID, ServerComputer computer) {
- remove(instanceID);
- computers.put(instanceID, computer);
- nextInstanceId = Math.max(nextInstanceId, instanceID + 1);
- }
+ void add(ServerComputer computer) {
+ var instanceID = computer.getInstanceID();
+ var instanceUUID = computer.getInstanceUUID();
- void remove(int instanceID) {
- var computer = get(instanceID);
- if (computer != null) {
- computer.unload();
- computer.onRemoved();
+ if (computersByInstanceId.containsKey(instanceID)) {
+ throw new IllegalStateException("Duplicate computer " + instanceID);
}
- computers.remove(instanceID);
+ if (computersByInstanceUuid.containsKey(instanceUUID)) {
+ throw new IllegalStateException("Duplicate computer " + instanceUUID);
+ }
+
+ computersByInstanceId.put(instanceID, computer);
+ computersByInstanceUuid.put(instanceUUID, computer);
+ }
+
+ void remove(ServerComputer computer) {
+ computer.unload();
+ computer.onRemoved();
+ computersByInstanceId.remove(computer.getInstanceID());
+ computersByInstanceUuid.remove(computer.getInstanceUUID());
}
void close() {
for (var computer : getComputers()) computer.unload();
- computers.clear();
+ computersByInstanceId.clear();
+ computersByInstanceUuid.clear();
}
public Collection getComputers() {
- return computers.values();
+ return computersByInstanceId.values();
}
}
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/inventory/ViewComputerMenu.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/inventory/ViewComputerMenu.java
index 1765c30a3..2dbb56262 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/computer/inventory/ViewComputerMenu.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/inventory/ViewComputerMenu.java
@@ -25,7 +25,7 @@ public class ViewComputerMenu extends ComputerMenuWithoutInventory {
private static boolean canInteractWith(ServerComputer computer, Player player) {
// If this computer no longer exists then discard it.
- if (ServerContext.get(computer.getLevel().getServer()).registry().get(computer.getInstanceID()) != computer) {
+ if (ServerContext.get(computer.getLevel().getServer()).registry().get(computer.getInstanceUUID()) != computer) {
return false;
}
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/network/client/ClientNetworkContext.java b/projects/common/src/main/java/dan200/computercraft/shared/network/client/ClientNetworkContext.java
index d981544d4..fce3cad03 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/network/client/ClientNetworkContext.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/network/client/ClientNetworkContext.java
@@ -8,6 +8,7 @@ import dan200.computercraft.shared.command.text.TableBuilder;
import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.computer.upload.UploadResult;
+import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
@@ -15,7 +16,6 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundEvent;
import javax.annotation.Nullable;
-import java.nio.ByteBuffer;
import java.util.UUID;
/**
@@ -30,11 +30,11 @@ public interface ClientNetworkContext {
void handlePlayRecord(BlockPos pos, @Nullable SoundEvent sound, @Nullable String name);
- void handlePocketComputerData(int instanceId, ComputerState state, int lightState, TerminalState terminal);
+ void handlePocketComputerData(UUID instanceId, ComputerState state, int lightState, TerminalState terminal);
- void handlePocketComputerDeleted(int instanceId);
+ void handlePocketComputerDeleted(UUID instanceId);
- void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, ByteBuffer audio);
+ void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, EncodedAudio audio);
void handleSpeakerMove(UUID source, SpeakerPosition.Message position);
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/network/client/PocketComputerDataMessage.java b/projects/common/src/main/java/dan200/computercraft/shared/network/client/PocketComputerDataMessage.java
index 2d032a800..b4be75181 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/network/client/PocketComputerDataMessage.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/network/client/PocketComputerDataMessage.java
@@ -13,24 +13,26 @@ import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.pocket.core.PocketServerComputer;
import net.minecraft.network.FriendlyByteBuf;
+import java.util.UUID;
+
/**
* Provides additional data about a client computer, such as its ID and current state.
*/
public class PocketComputerDataMessage implements NetworkMessage {
- private final int instanceId;
+ private final UUID clientId;
private final ComputerState state;
private final int lightState;
private final TerminalState terminal;
public PocketComputerDataMessage(PocketServerComputer computer, boolean sendTerminal) {
- instanceId = computer.getInstanceID();
+ clientId = computer.getInstanceUUID();
state = computer.getState();
lightState = computer.getLight();
terminal = sendTerminal ? computer.getTerminalState() : new TerminalState((NetworkedTerminal) null);
}
public PocketComputerDataMessage(FriendlyByteBuf buf) {
- instanceId = buf.readVarInt();
+ clientId = buf.readUUID();
state = buf.readEnum(ComputerState.class);
lightState = buf.readVarInt();
terminal = new TerminalState(buf);
@@ -38,7 +40,7 @@ public class PocketComputerDataMessage implements NetworkMessage {
- 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
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/network/client/SpeakerAudioClientMessage.java b/projects/common/src/main/java/dan200/computercraft/shared/network/client/SpeakerAudioClientMessage.java
index 6de8a0b57..283ff91d1 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/network/client/SpeakerAudioClientMessage.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/network/client/SpeakerAudioClientMessage.java
@@ -7,11 +7,11 @@ package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
+import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
import dan200.computercraft.shared.peripheral.speaker.SpeakerBlockEntity;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.network.FriendlyByteBuf;
-import java.nio.ByteBuffer;
import java.util.UUID;
/**
@@ -24,10 +24,10 @@ import java.util.UUID;
public class SpeakerAudioClientMessage implements NetworkMessage {
private final UUID source;
private final SpeakerPosition.Message pos;
- private final ByteBuffer content;
+ private final EncodedAudio content;
private final float volume;
- public SpeakerAudioClientMessage(UUID source, SpeakerPosition pos, float volume, ByteBuffer content) {
+ public SpeakerAudioClientMessage(UUID source, SpeakerPosition pos, float volume, EncodedAudio content) {
this.source = source;
this.pos = pos.asMessage();
this.content = content;
@@ -38,10 +38,7 @@ public class SpeakerAudioClientMessage implements NetworkMessage additionalTypes;
private final List methods;
- GenericPeripheral(BlockEntity tile, Direction side, @Nullable String name, Set additionalTypes, List methods) {
+ private @Nullable GuardedLuaContext contextWrapper;
+ private final GuardedLuaContext.Guard guard;
+
+ GenericPeripheral(BlockEntity tile, Direction side, String type, Set additionalTypes, List methods) {
this.side = side;
- var type = RegistryHelper.getKeyOrThrow(BuiltInRegistries.BLOCK_ENTITY_TYPE, tile.getType());
this.tile = tile;
- this.type = name != null ? name : type.toString();
+ this.type = type;
this.additionalTypes = additionalTypes;
this.methods = methods;
+ this.guard = () -> !tile.isRemoved() && tile.getLevel() != null && tile.getLevel().isLoaded(tile.getBlockPos());
}
public Direction side() {
@@ -50,7 +52,12 @@ public final class GenericPeripheral implements IDynamicPeripheral {
@Override
public MethodResult callMethod(IComputerAccess computer, ILuaContext context, int method, IArguments arguments) throws LuaException {
- return methods.get(method).apply(context, computer, arguments);
+ var contextWrapper = this.contextWrapper;
+ if (contextWrapper == null || !contextWrapper.wraps(context)) {
+ contextWrapper = this.contextWrapper = new GuardedLuaContext(context, guard);
+ }
+
+ return methods.get(method).apply(contextWrapper, computer, arguments);
}
@Override
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralBuilder.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralBuilder.java
index fc2457905..67d2ea8c4 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralBuilder.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheralBuilder.java
@@ -10,6 +10,9 @@ import dan200.computercraft.core.methods.NamedMethod;
import dan200.computercraft.core.methods.PeripheralMethod;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraft.world.level.block.entity.BlockEntityType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.util.ArrayList;
@@ -25,6 +28,8 @@ import java.util.Set;
* See the platform-specific peripheral providers for the usage of this.
*/
final class GenericPeripheralBuilder {
+ private static final Logger LOG = LoggerFactory.getLogger(GenericPeripheralBuilder.class);
+
private @Nullable String name;
private final Set additionalTypes = new HashSet<>(0);
private final ArrayList 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 info) {
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlock.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlock.java
index 06b874739..a5867437a 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlock.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlock.java
@@ -127,9 +127,8 @@ public class CableBlock extends Block implements SimpleWaterloggedBlock, EntityB
item = new ItemStack(ModRegistry.Items.CABLE.get());
}
- world.setBlock(pos, correctConnections(world, pos, newState), 3);
+ world.setBlockAndUpdate(pos, correctConnections(world, pos, newState));
- cable.modemChanged();
cable.connectionsChanged();
if (!world.isClientSide && !player.getAbilities().instabuild) {
Block.popResource(world, pos, item);
@@ -162,10 +161,7 @@ public class CableBlock extends Block implements SimpleWaterloggedBlock, EntityB
@Override
public void setPlacedBy(Level world, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) {
var tile = world.getBlockEntity(pos);
- if (tile instanceof CableBlockEntity cable) {
- if (cable.hasCable()) cable.connectionsChanged();
- }
-
+ if (tile instanceof CableBlockEntity cable && cable.hasCable()) cable.connectionsChanged();
super.setPlacedBy(world, pos, state, placer, stack);
}
@@ -177,14 +173,37 @@ public class CableBlock extends Block implements SimpleWaterloggedBlock, EntityB
@Override
@Deprecated
- public BlockState updateShape(BlockState state, Direction side, BlockState otherState, LevelAccessor world, BlockPos pos, BlockPos otherPos) {
- WaterloggableHelpers.updateShape(state, world, pos);
+ public BlockState updateShape(BlockState state, Direction side, BlockState otherState, LevelAccessor level, BlockPos pos, BlockPos otherPos) {
+ WaterloggableHelpers.updateShape(state, level, pos);
+
// Should never happen, but handle the case where we've no modem or cable.
if (!state.getValue(CABLE) && state.getValue(MODEM) == CableModemVariant.None) {
return getFluidState(state).createLegacyBlock();
}
- return world instanceof Level level ? state.setValue(CONNECTIONS.get(side), doesConnectVisually(state, level, pos, side)) : state;
+ // Pop our modem if needed.
+ var dir = state.getValue(MODEM).getFacing();
+ if (dir != null && dir.equals(side) && !canSupportCenter(level, otherPos, side.getOpposite())) {
+ // If we've no cable, follow normal Minecraft logic and just remove the block.
+ if (!state.getValue(CABLE)) return getFluidState(state).createLegacyBlock();
+
+ // Otherwise remove the cable and drop the modem manually.
+ state = state.setValue(CableBlock.MODEM, CableModemVariant.None);
+ if (level instanceof Level actualLevel) {
+ Block.popResource(actualLevel, pos, new ItemStack(ModRegistry.Items.WIRED_MODEM.get()));
+ }
+
+ if (level.getBlockEntity(pos) instanceof CableBlockEntity cable) cable.scheduleConnectionsChanged();
+ }
+
+ var modem = state.getValue(MODEM);
+ if (modem.getFacing() == side && modem.isPeripheralOn() && level.getBlockEntity(pos) instanceof CableBlockEntity cable) {
+ cable.queueRefreshPeripheral();
+ }
+
+ return level instanceof Level actualLevel
+ ? state.setValue(CONNECTIONS.get(side), doesConnectVisually(state, actualLevel, pos, side))
+ : state;
}
@Override
@@ -230,6 +249,7 @@ public class CableBlock extends Block implements SimpleWaterloggedBlock, EntityB
@Override
@Deprecated
public final InteractionResult use(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) {
+ if (player.isCrouching() || !player.mayBuild()) return InteractionResult.PASS;
return world.getBlockEntity(pos) instanceof CableBlockEntity modem ? modem.use(player) : InteractionResult.PASS;
}
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlockEntity.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlockEntity.java
index dca279888..21c1b7466 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlockEntity.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlockEntity.java
@@ -7,7 +7,6 @@ package dan200.computercraft.shared.peripheral.modem.wired;
import dan200.computercraft.api.network.wired.WiredElement;
import dan200.computercraft.api.network.wired.WiredNode;
import dan200.computercraft.api.peripheral.IPeripheral;
-import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.command.text.ChatHelpers;
import dan200.computercraft.shared.peripheral.modem.ModemState;
import dan200.computercraft.shared.platform.ComponentAccess;
@@ -20,21 +19,16 @@ import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
-import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
-import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3;
import javax.annotation.Nullable;
-import java.util.Map;
import java.util.Objects;
public class CableBlockEntity extends BlockEntity {
- private static final String NBT_PERIPHERAL_ENABLED = "PeripheralAccess";
-
private final class CableElement extends WiredModemElement {
@Override
public Level getLevel() {
@@ -57,33 +51,21 @@ public class CableBlockEntity extends BlockEntity {
}
}
- private boolean invalidPeripheral;
- private boolean peripheralAccessAllowed;
+ private boolean refreshPeripheral;
private final WiredModemLocalPeripheral peripheral = new WiredModemLocalPeripheral(PlatformHelper.get().createPeripheralAccess(this, x -> queueRefreshPeripheral()));
- private boolean connectionsFormed = false;
- private boolean connectionsChanged = false;
+ private boolean refreshConnections = false;
private final WiredModemElement cable = new CableElement();
private final WiredNode node = cable.getNode();
private final TickScheduler.Token tickToken = new TickScheduler.Token(this);
private final WiredModemPeripheral modem = new WiredModemPeripheral(
- new ModemState(() -> TickScheduler.schedule(tickToken)),
- cable
+ new ModemState(() -> TickScheduler.schedule(tickToken)), cable, peripheral, this
) {
- @Override
- protected WiredModemLocalPeripheral getLocalPeripheral() {
- return peripheral;
- }
-
@Override
public Vec3 getPosition() {
- return Vec3.atCenterOf(getBlockPos().relative(getDirection()));
- }
-
- @Override
- public Object getTarget() {
- return CableBlockEntity.this;
+ var dir = getModemDirection();
+ return Vec3.atCenterOf(dir == null ? getBlockPos() : getBlockPos().relative(dir));
}
};
@@ -93,93 +75,61 @@ public class CableBlockEntity extends BlockEntity {
super(type, pos, state);
}
- private void onRemove() {
- if (level == null || !level.isClientSide) {
- node.remove();
- connectionsFormed = false;
- }
- }
-
@Override
public void setRemoved() {
super.setRemoved();
modem.removed();
- onRemove();
+ if (level == null || !level.isClientSide) node.remove();
}
@Override
public void clearRemoved() {
super.clearRemoved();
+ refreshConnections = refreshPeripheral = true;
TickScheduler.schedule(tickToken);
}
@Override
@Deprecated
public void setBlockState(BlockState state) {
- var direction = getMaybeDirection();
+ var direction = getModemDirection();
+ var hasCable = hasCable();
super.setBlockState(state);
- // We invalidate both the modem and element if the modem's direction is different.
- if (getMaybeDirection() != direction) PlatformHelper.get().invalidateComponent(this);
+ // We invalidate both the modem and element if the modem direction or cable are different.
+ if (hasCable() != hasCable || getModemDirection() != direction) PlatformHelper.get().invalidateComponent(this);
}
@Nullable
- private Direction getMaybeDirection() {
+ private Direction getModemDirection() {
return getBlockState().getValue(CableBlock.MODEM).getFacing();
}
- private Direction getDirection() {
- var direction = getMaybeDirection();
- return direction == null ? Direction.NORTH : direction;
- }
-
void neighborChanged(BlockPos neighbour) {
- var dir = getDirection();
- if (neighbour.equals(getBlockPos().relative(dir)) && hasModem() && !getBlockState().canSurvive(getLevel(), getBlockPos())) {
- if (hasCable()) {
- // Drop the modem and convert to cable
- Block.popResource(getLevel(), getBlockPos(), new ItemStack(ModRegistry.Items.WIRED_MODEM.get()));
- getLevel().setBlockAndUpdate(getBlockPos(), getBlockState().setValue(CableBlock.MODEM, CableModemVariant.None));
- modemChanged();
- connectionsChanged();
- } else {
- // Drop everything and remove block
- Block.popResource(getLevel(), getBlockPos(), new ItemStack(ModRegistry.Items.WIRED_MODEM.get()));
- getLevel().removeBlock(getBlockPos(), false);
- // This'll call #destroy(), so we don't need to reset the network here.
- }
-
- return;
- }
-
- if (!level.isClientSide && peripheralAccessAllowed) {
- var facing = getDirection();
- if (getBlockPos().relative(facing).equals(neighbour)) queueRefreshPeripheral();
+ var dir = getModemDirection();
+ if (!level.isClientSide && dir != null && getBlockPos().relative(dir).equals(neighbour) && isPeripheralOn()) {
+ queueRefreshPeripheral();
}
}
- private void queueRefreshPeripheral() {
- if (invalidPeripheral) return;
- invalidPeripheral = true;
+ void queueRefreshPeripheral() {
+ refreshPeripheral = true;
TickScheduler.schedule(tickToken);
}
- private void refreshPeripheral() {
- invalidPeripheral = false;
- if (level != null && !isRemoved() && peripheral.attach(level, getBlockPos(), getDirection())) {
- updateConnectedPeripherals();
- }
- }
-
InteractionResult use(Player player) {
- if (player.isCrouching() || !player.mayBuild()) return InteractionResult.PASS;
if (!canAttachPeripheral()) return InteractionResult.FAIL;
if (getLevel().isClientSide) return InteractionResult.SUCCESS;
var oldName = peripheral.getConnectedName();
- togglePeripheralAccess();
+ if (isPeripheralOn()) {
+ detachPeripheral();
+ } else {
+ attachPeripheral();
+ }
var newName = peripheral.getConnectedName();
+
if (!Objects.equals(newName, oldName)) {
if (oldName != null) {
player.displayClientMessage(Component.translatable("chat.computercraft.wired_modem.peripheral_disconnected",
@@ -197,14 +147,11 @@ public class CableBlockEntity extends BlockEntity {
@Override
public void load(CompoundTag nbt) {
super.load(nbt);
- // Fallback to the previous (incorrect) key
- peripheralAccessAllowed = nbt.getBoolean(NBT_PERIPHERAL_ENABLED) || nbt.getBoolean("PeirpheralAccess");
peripheral.read(nbt, "");
}
@Override
public void saveAdditional(CompoundTag nbt) {
- nbt.putBoolean(NBT_PERIPHERAL_ENABLED, peripheralAccessAllowed);
peripheral.write(nbt, "");
super.saveAdditional(nbt);
}
@@ -213,7 +160,7 @@ public class CableBlockEntity extends BlockEntity {
var state = getBlockState();
var oldVariant = state.getValue(CableBlock.MODEM);
var newVariant = CableModemVariant
- .from(oldVariant.getFacing(), modem.getModemState().isOpen(), peripheralAccessAllowed);
+ .from(oldVariant.getFacing(), modem.getModemState().isOpen(), peripheral.hasPeripheral());
if (oldVariant != newVariant) {
level.setBlockAndUpdate(getBlockPos(), state.setValue(CableBlock.MODEM, newVariant));
@@ -223,31 +170,24 @@ public class CableBlockEntity extends BlockEntity {
void blockTick() {
if (getLevel().isClientSide) return;
- if (invalidPeripheral) refreshPeripheral();
+ if (refreshPeripheral) {
+ refreshPeripheral = false;
+ if (isPeripheralOn()) attachPeripheral();
+ }
if (modem.getModemState().pollChanged()) updateBlockState();
- if (!connectionsFormed) {
- connectionsFormed = true;
-
- connectionsChanged();
- if (peripheralAccessAllowed) {
- peripheral.attach(level, worldPosition, getDirection());
- updateConnectedPeripherals();
- }
- }
-
- if (connectionsChanged) connectionsChanged();
+ if (refreshConnections) connectionsChanged();
}
- private void scheduleConnectionsChanged() {
- connectionsChanged = true;
+ void scheduleConnectionsChanged() {
+ refreshConnections = true;
TickScheduler.schedule(tickToken);
}
void connectionsChanged() {
if (getLevel().isClientSide) return;
- connectionsChanged = false;
+ refreshConnections = false;
var state = getBlockState();
var world = getLevel();
@@ -263,56 +203,29 @@ public class CableBlockEntity extends BlockEntity {
if (CableBlock.canConnectIn(state, facing)) {
// If we can connect to it then do so
this.node.connectTo(node);
- } else if (this.node.getNetwork() == node.getNetwork()) {
- // Otherwise if we're on the same network then attempt to void it.
+ } else {
+ // Otherwise break the connection.
this.node.disconnectFrom(node);
}
}
+
+ // If we can no longer attach peripherals, then detach any which may have existed
+ if (!canAttachPeripheral()) detachPeripheral();
}
- void modemChanged() {
- // Tell anyone who cares that the connection state has changed
- PlatformHelper.get().invalidateComponent(this);
-
- if (getLevel().isClientSide) return;
-
- // If we can no longer attach peripherals, then detach any
- // which may have existed
- if (!canAttachPeripheral() && peripheralAccessAllowed) {
- peripheralAccessAllowed = false;
- peripheral.detach();
- node.updatePeripherals(Map.of());
- setChanged();
- updateBlockState();
- }
+ private void attachPeripheral() {
+ var dir = Objects.requireNonNull(getModemDirection(), "Attaching without a modem");
+ if (peripheral.attach(getLevel(), getBlockPos(), dir)) updateConnectedPeripherals();
+ updateBlockState();
}
- private void togglePeripheralAccess() {
- if (!peripheralAccessAllowed) {
- peripheral.attach(level, getBlockPos(), getDirection());
- if (!peripheral.hasPeripheral()) return;
-
- peripheralAccessAllowed = true;
- node.updatePeripherals(peripheral.toMap());
- } else {
- peripheral.detach();
-
- peripheralAccessAllowed = false;
- node.updatePeripherals(Map.of());
- }
-
+ private void detachPeripheral() {
+ if (peripheral.detach()) updateConnectedPeripherals();
updateBlockState();
}
private void updateConnectedPeripherals() {
- var peripherals = peripheral.toMap();
- if (peripherals.isEmpty()) {
- // If there are no peripherals then disable access and update the display state.
- peripheralAccessAllowed = false;
- updateBlockState();
- }
-
- node.updatePeripherals(peripherals);
+ node.updatePeripherals(peripheral.toMap());
}
@Nullable
@@ -322,7 +235,11 @@ public class CableBlockEntity extends BlockEntity {
@Nullable
public IPeripheral getPeripheral(@Nullable Direction direction) {
- return direction == null || getMaybeDirection() == direction ? modem : null;
+ return direction == null || getModemDirection() == direction ? modem : null;
+ }
+
+ private boolean isPeripheralOn() {
+ return getBlockState().getValue(CableBlock.MODEM).isPeripheralOn();
}
boolean hasCable() {
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlockItem.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlockItem.java
index 27ea33c6c..cae36c886 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlockItem.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlockItem.java
@@ -31,15 +31,12 @@ public abstract class CableBlockItem extends BlockItem {
// TODO: Check entity collision.
if (!state.canSurvive(world, pos)) return false;
- world.setBlock(pos, state, 3);
+ world.setBlockAndUpdate(pos, state);
var soundType = state.getBlock().getSoundType(state);
world.playSound(null, pos, soundType.getPlaceSound(), SoundSource.BLOCKS, (soundType.getVolume() + 1.0F) / 2.0F, soundType.getPitch() * 0.8F);
var tile = world.getBlockEntity(pos);
- if (tile instanceof CableBlockEntity cable) {
- cable.modemChanged();
- cable.connectionsChanged();
- }
+ if (tile instanceof CableBlockEntity cable) cable.connectionsChanged();
return true;
}
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableModemVariant.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableModemVariant.java
index 89422d12d..e27749b0e 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableModemVariant.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableModemVariant.java
@@ -10,49 +10,57 @@ import net.minecraft.util.StringRepresentable;
import javax.annotation.Nullable;
public enum CableModemVariant implements StringRepresentable {
- None("none", null),
- DownOff("down_off", Direction.DOWN),
- UpOff("up_off", Direction.UP),
- NorthOff("north_off", Direction.NORTH),
- SouthOff("south_off", Direction.SOUTH),
- WestOff("west_off", Direction.WEST),
- EastOff("east_off", Direction.EAST),
- DownOn("down_on", Direction.DOWN),
- UpOn("up_on", Direction.UP),
- NorthOn("north_on", Direction.NORTH),
- SouthOn("south_on", Direction.SOUTH),
- WestOn("west_on", Direction.WEST),
- EastOn("east_on", Direction.EAST),
- DownOffPeripheral("down_off_peripheral", Direction.DOWN),
- UpOffPeripheral("up_off_peripheral", Direction.UP),
- NorthOffPeripheral("north_off_peripheral", Direction.NORTH),
- SouthOffPeripheral("south_off_peripheral", Direction.SOUTH),
- WestOffPeripheral("west_off_peripheral", Direction.WEST),
- EastOffPeripheral("east_off_peripheral", Direction.EAST),
- DownOnPeripheral("down_on_peripheral", Direction.DOWN),
- UpOnPeripheral("up_on_peripheral", Direction.UP),
- NorthOnPeripheral("north_on_peripheral", Direction.NORTH),
- SouthOnPeripheral("south_on_peripheral", Direction.SOUTH),
- WestOnPeripheral("west_on_peripheral", Direction.WEST),
- EastOnPeripheral("east_on_peripheral", Direction.EAST);
+ None("none", null, false, false),
+ DownOff("down_off", Direction.DOWN, false, false),
+ UpOff("up_off", Direction.UP, false, false),
+ NorthOff("north_off", Direction.NORTH, false, false),
+ SouthOff("south_off", Direction.SOUTH, false, false),
+ WestOff("west_off", Direction.WEST, false, false),
+ EastOff("east_off", Direction.EAST, false, false),
+ DownOn("down_on", Direction.DOWN, true, false),
+ UpOn("up_on", Direction.UP, true, false),
+ NorthOn("north_on", Direction.NORTH, true, false),
+ SouthOn("south_on", Direction.SOUTH, true, false),
+ WestOn("west_on", Direction.WEST, true, false),
+ EastOn("east_on", Direction.EAST, true, false),
+ DownOffPeripheral("down_off_peripheral", Direction.DOWN, false, true),
+ UpOffPeripheral("up_off_peripheral", Direction.UP, false, true),
+ NorthOffPeripheral("north_off_peripheral", Direction.NORTH, false, true),
+ SouthOffPeripheral("south_off_peripheral", Direction.SOUTH, false, true),
+ WestOffPeripheral("west_off_peripheral", Direction.WEST, false, true),
+ EastOffPeripheral("east_off_peripheral", Direction.EAST, false, true),
+ DownOnPeripheral("down_on_peripheral", Direction.DOWN, true, true),
+ UpOnPeripheral("up_on_peripheral", Direction.UP, true, true),
+ NorthOnPeripheral("north_on_peripheral", Direction.NORTH, true, true),
+ SouthOnPeripheral("south_on_peripheral", Direction.SOUTH, true, true),
+ WestOnPeripheral("west_on_peripheral", Direction.WEST, true, true),
+ EastOnPeripheral("east_on_peripheral", Direction.EAST, true, true);
private static final CableModemVariant[] VALUES = values();
private final String name;
private final @Nullable Direction facing;
+ private final boolean modemOn, peripheralOn;
- CableModemVariant(String name, @Nullable Direction facing) {
+ CableModemVariant(String name, @Nullable Direction facing, boolean modemOn, boolean peripheralOn) {
this.name = name;
this.facing = facing;
+ this.modemOn = modemOn;
+ this.peripheralOn = peripheralOn;
+ if (ordinal() != getIndex(facing, modemOn, peripheralOn)) throw new IllegalStateException("Mismatched ordinal");
}
public static CableModemVariant from(Direction facing) {
- return facing == null ? None : VALUES[1 + facing.get3DDataValue()];
+ return VALUES[1 + facing.get3DDataValue()];
+ }
+
+ private static int getIndex(@Nullable Direction facing, boolean modem, boolean peripheral) {
+ var state = (modem ? 1 : 0) + (peripheral ? 2 : 0);
+ return facing == null ? 0 : 1 + 6 * state + facing.get3DDataValue();
}
public static CableModemVariant from(@Nullable Direction facing, boolean modem, boolean peripheral) {
- var state = (modem ? 1 : 0) + (peripheral ? 2 : 0);
- return facing == null ? None : VALUES[1 + 6 * state + facing.get3DDataValue()];
+ return VALUES[getIndex(facing, modem, peripheral)];
}
@Override
@@ -64,6 +72,14 @@ public enum CableModemVariant implements StringRepresentable {
return facing;
}
+ public boolean isModemOn() {
+ return modemOn;
+ }
+
+ public boolean isPeripheralOn() {
+ return peripheralOn;
+ }
+
@Override
public String toString() {
return name;
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemFullBlock.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemFullBlock.java
index 8707ca5e9..6f9e0b958 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemFullBlock.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemFullBlock.java
@@ -7,12 +7,14 @@ package dan200.computercraft.shared.peripheral.modem.wired;
import dan200.computercraft.annotations.ForgeOverride;
import dan200.computercraft.shared.ModRegistry;
import net.minecraft.core.BlockPos;
+import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.RandomSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
+import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.EntityBlock;
@@ -49,13 +51,27 @@ public class WiredModemFullBlock extends Block implements EntityBlock {
@Override
@Deprecated
- public final void neighborChanged(BlockState state, Level world, BlockPos pos, Block neighbourBlock, BlockPos neighbourPos, boolean isMoving) {
- if (world.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) modem.neighborChanged(neighbourPos);
+ public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor level, BlockPos pos, BlockPos neighborPos) {
+ if (state.getValue(PERIPHERAL_ON) && level.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) {
+ modem.queueRefreshPeripheral(direction);
+ }
+
+ return super.updateShape(state, direction, neighborState, level, pos, neighborPos);
+ }
+
+ @Override
+ @Deprecated
+ public final void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighbourBlock, BlockPos neighbourPos, boolean isMoving) {
+ if (state.getValue(PERIPHERAL_ON) && level.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) {
+ modem.neighborChanged(neighbourPos);
+ }
}
@ForgeOverride
- public final void onNeighborChange(BlockState state, LevelReader world, BlockPos pos, BlockPos neighbour) {
- if (world.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) modem.neighborChanged(neighbour);
+ public final void onNeighborChange(BlockState state, LevelReader level, BlockPos pos, BlockPos neighbour) {
+ if (state.getValue(PERIPHERAL_ON) && level.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) {
+ modem.neighborChanged(neighbour);
+ }
}
@Override
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemFullBlockEntity.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemFullBlockEntity.java
index af92d6fe8..288b0e1cd 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemFullBlockEntity.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemFullBlockEntity.java
@@ -32,8 +32,6 @@ import static dan200.computercraft.shared.peripheral.modem.wired.WiredModemFullB
import static dan200.computercraft.shared.peripheral.modem.wired.WiredModemFullBlock.PERIPHERAL_ON;
public class WiredModemFullBlockEntity extends BlockEntity {
- private static final String NBT_PERIPHERAL_ENABLED = "PeripheralAccess";
-
private static final class FullElement extends WiredModemElement {
private final WiredModemFullBlockEntity entity;
@@ -70,11 +68,9 @@ public class WiredModemFullBlockEntity extends BlockEntity {
private final WiredModemPeripheral[] modems = new WiredModemPeripheral[6];
- private boolean peripheralAccessAllowed = false;
private final WiredModemLocalPeripheral[] peripherals = new WiredModemLocalPeripheral[6];
- private boolean connectionsFormed = false;
- private boolean connectionsChanged = false;
+ private boolean refreshConnections = false;
private final TickScheduler.Token tickToken = new TickScheduler.Token(this);
private final ModemState modemState = new ModemState(() -> TickScheduler.schedule(tickToken));
@@ -96,31 +92,30 @@ public class WiredModemFullBlockEntity extends BlockEntity {
@Override
public void setRemoved() {
super.setRemoved();
- if (level == null || !level.isClientSide) {
- node.remove();
- connectionsFormed = false;
+
+ for (var modem : modems) {
+ if (modem != null) modem.removed();
}
+ if (level == null || !level.isClientSide) node.remove();
+ }
+
+ @Override
+ public void clearRemoved() {
+ super.clearRemoved();
+ refreshConnections = true;
+ invalidSides = DirectionUtil.ALL_SIDES;
+ TickScheduler.schedule(tickToken);
}
void neighborChanged(BlockPos neighbour) {
- if (!level.isClientSide && peripheralAccessAllowed) {
- for (var facing : DirectionUtil.FACINGS) {
- if (getBlockPos().relative(facing).equals(neighbour)) queueRefreshPeripheral(facing);
- }
+ for (var facing : DirectionUtil.FACINGS) {
+ if (getBlockPos().relative(facing).equals(neighbour)) queueRefreshPeripheral(facing);
}
}
- private void queueRefreshPeripheral(Direction facing) {
- if (invalidSides == 0) TickScheduler.schedule(tickToken);
+ void queueRefreshPeripheral(Direction facing) {
invalidSides |= 1 << facing.ordinal();
- }
-
- private void refreshPeripheral(Direction facing) {
- invalidSides &= ~(1 << facing.ordinal());
- var peripheral = peripherals[facing.ordinal()];
- if (level != null && !isRemoved() && peripheral.attach(level, getBlockPos(), facing)) {
- updateConnectedPeripherals();
- }
+ TickScheduler.schedule(tickToken);
}
public InteractionResult use(Player player) {
@@ -129,7 +124,11 @@ public class WiredModemFullBlockEntity extends BlockEntity {
// On server, we interacted if a peripheral was found
var oldPeriphNames = getConnectedPeripheralNames();
- togglePeripheralAccess();
+ if (isPeripheralOn()) {
+ detachPeripherals();
+ } else {
+ attachPeripherals(DirectionUtil.ALL_SIDES);
+ }
var periphNames = getConnectedPeripheralNames();
if (!Objects.equals(periphNames, oldPeriphNames)) {
@@ -158,65 +157,45 @@ public class WiredModemFullBlockEntity extends BlockEntity {
@Override
public void load(CompoundTag nbt) {
super.load(nbt);
- peripheralAccessAllowed = nbt.getBoolean(NBT_PERIPHERAL_ENABLED);
for (var i = 0; i < peripherals.length; i++) peripherals[i].read(nbt, Integer.toString(i));
}
@Override
public void saveAdditional(CompoundTag nbt) {
- nbt.putBoolean(NBT_PERIPHERAL_ENABLED, peripheralAccessAllowed);
for (var i = 0; i < peripherals.length; i++) peripherals[i].write(nbt, Integer.toString(i));
super.saveAdditional(nbt);
}
- private void updateBlockState() {
- var state = getBlockState();
- boolean modemOn = modemState.isOpen(), peripheralOn = peripheralAccessAllowed;
- if (state.getValue(MODEM_ON) == modemOn && state.getValue(PERIPHERAL_ON) == peripheralOn) return;
-
- getLevel().setBlockAndUpdate(getBlockPos(), state.setValue(MODEM_ON, modemOn).setValue(PERIPHERAL_ON, peripheralOn));
- }
-
- @Override
- public void clearRemoved() {
- super.clearRemoved();
- TickScheduler.schedule(tickToken);
- }
-
void blockTick() {
if (getLevel().isClientSide) return;
if (invalidSides != 0) {
- for (var direction : DirectionUtil.FACINGS) {
- if ((invalidSides & (1 << direction.ordinal())) != 0) refreshPeripheral(direction);
- }
+ var oldInvalidSides = invalidSides;
+ invalidSides = 0;
+ if (isPeripheralOn()) attachPeripherals(oldInvalidSides);
}
- if (modemState.pollChanged()) updateBlockState();
+ if (modemState.pollChanged()) updateModemBlockState();
- if (!connectionsFormed) {
- connectionsFormed = true;
+ if (refreshConnections) connectionsChanged();
+ }
- connectionsChanged();
- if (peripheralAccessAllowed) {
- for (var facing : DirectionUtil.FACINGS) {
- peripherals[facing.ordinal()].attach(level, getBlockPos(), facing);
- }
- updateConnectedPeripherals();
- }
- }
+ private void updateModemBlockState() {
+ var state = getBlockState();
+ var modemOn = modemState.isOpen();
+ if (state.getValue(MODEM_ON) == modemOn) return;
- if (connectionsChanged) connectionsChanged();
+ getLevel().setBlockAndUpdate(getBlockPos(), state.setValue(MODEM_ON, modemOn));
}
private void scheduleConnectionsChanged() {
- connectionsChanged = true;
+ refreshConnections = true;
TickScheduler.schedule(tickToken);
}
private void connectionsChanged() {
if (getLevel().isClientSide) return;
- connectionsChanged = false;
+ refreshConnections = false;
var world = getLevel();
var current = getBlockPos();
@@ -231,57 +210,48 @@ public class WiredModemFullBlockEntity extends BlockEntity {
}
}
- private void togglePeripheralAccess() {
- if (!peripheralAccessAllowed) {
- var hasAny = false;
- for (var facing : DirectionUtil.FACINGS) {
- var peripheral = peripherals[facing.ordinal()];
- peripheral.attach(level, getBlockPos(), facing);
- hasAny |= peripheral.hasPeripheral();
- }
-
- if (!hasAny) return;
-
- peripheralAccessAllowed = true;
- node.updatePeripherals(getConnectedPeripherals());
- } else {
- peripheralAccessAllowed = false;
-
- for (var peripheral : peripherals) peripheral.detach();
- node.updatePeripherals(Map.of());
- }
-
- updateBlockState();
- }
-
- private Set getConnectedPeripheralNames() {
- if (!peripheralAccessAllowed) return Set.of();
-
- Set peripherals = new HashSet<>(6);
+ private List getConnectedPeripheralNames() {
+ List 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 getConnectedPeripherals() {
- if (!peripheralAccessAllowed) return Map.of();
+ private void attachPeripherals(int sides) {
+ var anyChanged = false;
- Map peripherals = new HashMap<>(6);
- for (var peripheral : this.peripherals) peripheral.extendMap(peripherals);
- return Collections.unmodifiableMap(peripherals);
- }
+ Map attachedPeripherals = new HashMap<>(6);
- private void updateConnectedPeripherals() {
- var peripherals = getConnectedPeripherals();
- if (peripherals.isEmpty()) {
- // If there are no peripherals then disable access and update the display state.
- peripheralAccessAllowed = false;
- updateBlockState();
+ for (var facing : DirectionUtil.FACINGS) {
+ var peripheral = peripherals[facing.ordinal()];
+ if (DirectionUtil.isSet(sides, facing)) anyChanged |= peripheral.attach(getLevel(), getBlockPos(), facing);
+ peripheral.extendMap(attachedPeripherals);
}
- node.updatePeripherals(peripherals);
+ if (anyChanged) node.updatePeripherals(attachedPeripherals);
+
+ updatePeripheralBlocKState(!attachedPeripherals.isEmpty());
+ }
+
+ private void detachPeripherals() {
+ var anyChanged = false;
+ for (var peripheral : peripherals) anyChanged |= peripheral.detach();
+ if (anyChanged) node.updatePeripherals(Map.of());
+
+ updatePeripheralBlocKState(false);
+ }
+
+ private void updatePeripheralBlocKState(boolean peripheralOn) {
+ var state = getBlockState();
+ if (state.getValue(PERIPHERAL_ON) == peripheralOn) return;
+ getLevel().setBlockAndUpdate(getBlockPos(), state.setValue(PERIPHERAL_ON, peripheralOn));
+ }
+
+ private boolean isPeripheralOn() {
+ return getBlockState().getValue(PERIPHERAL_ON);
}
public WiredElement getElement() {
@@ -295,22 +265,11 @@ public class WiredModemFullBlockEntity extends BlockEntity {
var peripheral = modems[side.ordinal()];
if (peripheral != null) return peripheral;
- var localPeripheral = peripherals[side.ordinal()];
- return modems[side.ordinal()] = new WiredModemPeripheral(modemState, element) {
- @Override
- protected WiredModemLocalPeripheral getLocalPeripheral() {
- return localPeripheral;
- }
-
+ return modems[side.ordinal()] = new WiredModemPeripheral(modemState, element, peripherals[side.ordinal()], this) {
@Override
public Vec3 getPosition() {
return Vec3.atCenterOf(getBlockPos().relative(side));
}
-
- @Override
- public Object getTarget() {
- return WiredModemFullBlockEntity.this;
- }
};
}
}
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemLocalPeripheral.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemLocalPeripheral.java
index 1f4ac77b4..3893b4ac4 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemLocalPeripheral.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemLocalPeripheral.java
@@ -6,6 +6,7 @@ package dan200.computercraft.shared.peripheral.modem.wired;
import dan200.computercraft.api.ComputerCraftTags;
import dan200.computercraft.api.peripheral.IPeripheral;
+import dan200.computercraft.core.util.PeripheralHelpers;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.platform.ComponentAccess;
import net.minecraft.core.BlockPos;
@@ -15,7 +16,6 @@ import net.minecraft.nbt.Tag;
import net.minecraft.world.level.Level;
import javax.annotation.Nullable;
-import java.util.Collections;
import java.util.Map;
import static dan200.computercraft.core.util.Nullability.assertNonNull;
@@ -66,7 +66,7 @@ public final class WiredModemLocalPeripheral {
this.id = ServerContext.get(assertNonNull(world.getServer())).getNextId("peripheral." + type);
}
- return oldPeripheral == null || !oldPeripheral.equals(peripheral);
+ return !PeripheralHelpers.equals(oldPeripheral, peripheral);
}
}
@@ -86,11 +86,6 @@ public final class WiredModemLocalPeripheral {
return peripheral != null ? type + "_" + id : null;
}
- @Nullable
- public IPeripheral getPeripheral() {
- return peripheral;
- }
-
public boolean hasPeripheral() {
return peripheral != null;
}
@@ -100,9 +95,7 @@ public final class WiredModemLocalPeripheral {
}
public Map 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) {
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java
index 96e2ef1ed..ce1374b27 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java
@@ -15,6 +15,7 @@ import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.peripheral.NotAttachedException;
import dan200.computercraft.api.peripheral.WorkMonitor;
import dan200.computercraft.core.apis.PeripheralAPI;
+import dan200.computercraft.core.computer.GuardedLuaContext;
import dan200.computercraft.core.methods.PeripheralMethod;
import dan200.computercraft.core.util.LuaUtil;
import dan200.computercraft.shared.computer.core.ServerContext;
@@ -22,6 +23,7 @@ import dan200.computercraft.shared.peripheral.modem.ModemPeripheral;
import dan200.computercraft.shared.peripheral.modem.ModemState;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.entity.BlockEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -34,12 +36,21 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
private static final Logger LOG = LoggerFactory.getLogger(WiredModemPeripheral.class);
private final WiredModemElement modem;
+ private final WiredModemLocalPeripheral localPeripheral;
+ private final BlockEntity target;
private final Map> peripheralWrappers = new HashMap<>(1);
- public WiredModemPeripheral(ModemState state, WiredModemElement modem) {
+ public WiredModemPeripheral(
+ ModemState state,
+ WiredModemElement modem,
+ WiredModemLocalPeripheral localPeripheral,
+ BlockEntity target
+ ) {
super(state);
this.modem = modem;
+ this.localPeripheral = localPeripheral;
+ this.target = target;
}
//region IPacketSender implementation
@@ -62,8 +73,6 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
public Level getLevel() {
return modem.getLevel();
}
-
- protected abstract WiredModemLocalPeripheral getLocalPeripheral();
//endregion
@Override
@@ -207,7 +216,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
*/
@LuaFunction
public final @Nullable Object[] getNameLocal() {
- var local = getLocalPeripheral().getConnectedName();
+ var local = localPeripheral.getConnectedName();
return local == null ? null : new Object[]{ local };
}
@@ -218,8 +227,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
ConcurrentMap wrappers;
synchronized (peripheralWrappers) {
- wrappers = peripheralWrappers.get(computer);
- if (wrappers == null) peripheralWrappers.put(computer, wrappers = new ConcurrentHashMap<>());
+ wrappers = peripheralWrappers.computeIfAbsent(computer, k -> new ConcurrentHashMap<>());
}
synchronized (modem.getRemotePeripherals()) {
@@ -245,11 +253,13 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
}
@Override
- public boolean equals(@Nullable IPeripheral other) {
- if (other instanceof WiredModemPeripheral otherModem) {
- return otherModem.modem == modem;
- }
- return false;
+ public final boolean equals(@Nullable IPeripheral other) {
+ return other instanceof WiredModemPeripheral otherModem && otherModem.modem == modem;
+ }
+
+ @Override
+ public final Object getTarget() {
+ return target;
}
//endregion
@@ -272,12 +282,11 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
var wrapper = wrappers.remove(name);
if (wrapper != null) wrapper.detach();
}
-
}
}
private void attachPeripheralImpl(IComputerAccess computer, ConcurrentMap peripherals, String periphName, IPeripheral peripheral) {
- if (!peripherals.containsKey(periphName) && !periphName.equals(getLocalPeripheral().getConnectedName())) {
+ if (!peripherals.containsKey(periphName) && !periphName.equals(localPeripheral.getConnectedName())) {
var methods = ServerContext.get(((ServerLevel) getLevel()).getServer()).peripheralMethods().getSelfMethods(peripheral);
var wrapper = new RemotePeripheralWrapper(modem, peripheral, computer, periphName, methods);
peripherals.put(periphName, wrapper);
@@ -296,7 +305,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
return wrappers == null ? null : wrappers.get(remoteName);
}
- private static class RemotePeripheralWrapper implements IComputerAccess {
+ private static class RemotePeripheralWrapper implements IComputerAccess, GuardedLuaContext.Guard {
private final WiredModemElement element;
private final IPeripheral peripheral;
private final IComputerAccess computer;
@@ -309,6 +318,8 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
private volatile boolean attached;
private final Set mounts = new HashSet<>();
+ private @Nullable GuardedLuaContext contextWrapper;
+
RemotePeripheralWrapper(WiredModemElement element, IPeripheral peripheral, IComputerAccess computer, String name, Map methods) {
this.element = element;
this.peripheral = peripheral;
@@ -356,7 +367,19 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
public MethodResult callMethod(ILuaContext context, String methodName, IArguments arguments) throws LuaException {
var method = methodMap.get(methodName);
if (method == null) throw new LuaException("No such method " + methodName);
- return method.apply(peripheral, context, this, arguments);
+
+ // Wrap the ILuaContext. We try to reuse the previous context where possible to avoid allocations.
+ var contextWrapper = this.contextWrapper;
+ if (contextWrapper == null || !contextWrapper.wraps(context)) {
+ contextWrapper = this.contextWrapper = new GuardedLuaContext(context, this);
+ }
+
+ return method.apply(peripheral, contextWrapper, this, arguments);
+ }
+
+ @Override
+ public boolean checkValid() {
+ return attached;
}
// IComputerAccess implementation
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorBlockEntity.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorBlockEntity.java
index 4e3c59cf6..bd83918c1 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorBlockEntity.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorBlockEntity.java
@@ -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 computers = Collections.newSetFromMap(new ConcurrentHashMap<>());
private boolean needsUpdate = false;
private boolean needsValidating = false;
- private boolean destroyed = false;
// MonitorWatcher state.
boolean enqueued;
@@ -89,7 +94,7 @@ public class MonitorBlockEntity extends BlockEntity {
@Override
public void setRemoved() {
super.setRemoved();
- if (clientMonitor != null && xIndex == 0 && yIndex == 0) clientMonitor.destroy();
+ if (clientMonitor != null) clientMonitor.destroy();
}
@Override
@@ -143,7 +148,7 @@ public class MonitorBlockEntity extends BlockEntity {
private ServerMonitor getServerMonitor() {
if (serverMonitor != null) return serverMonitor;
- var origin = getOrigin().getMonitor();
+ var origin = getOrigin();
if (origin == null) return null;
return serverMonitor = origin.serverMonitor;
@@ -182,13 +187,11 @@ public class MonitorBlockEntity extends BlockEntity {
}
@Nullable
- public ClientMonitor getClientMonitor() {
+ public ClientMonitor getOriginClientMonitor() {
if (clientMonitor != null) return clientMonitor;
- var te = level.getBlockEntity(toWorldPos(0, 0));
- if (!(te instanceof MonitorBlockEntity monitor)) return null;
-
- return clientMonitor = monitor.clientMonitor;
+ var origin = getOrigin();
+ return origin == null ? null : origin.clientMonitor;
}
// Networking stuff
@@ -209,17 +212,14 @@ public class MonitorBlockEntity extends BlockEntity {
}
private void onClientLoad(int oldXIndex, int oldYIndex) {
- if (oldXIndex != xIndex || oldYIndex != yIndex) {
- // If our index has changed then it's possible the origin monitor has changed. Thus
- // we'll clear our cache. If we're the origin then we'll need to remove the glList as well.
- if (oldXIndex == 0 && oldYIndex == 0 && clientMonitor != null) clientMonitor.destroy();
+ if ((oldXIndex != xIndex || oldYIndex != yIndex) && clientMonitor != null) {
+ // If our index has changed, and we were the origin, then destroy the current monitor.
+ clientMonitor.destroy();
clientMonitor = null;
}
- if (xIndex == 0 && yIndex == 0) {
- // If we're the origin terminal then create it.
- if (clientMonitor == null) clientMonitor = new ClientMonitor(this);
- }
+ // If we're the origin terminal then create it.
+ if (xIndex == 0 && yIndex == 0 && clientMonitor == null) clientMonitor = new ClientMonitor(this);
}
public final void read(TerminalState state) {
@@ -286,7 +286,7 @@ public class MonitorBlockEntity extends BlockEntity {
}
boolean isCompatible(MonitorBlockEntity other) {
- return !other.destroyed && advanced == other.advanced && getOrientation() == other.getOrientation() && getDirection() == other.getDirection();
+ return advanced == other.advanced && getOrientation() == other.getOrientation() && getDirection() == other.getDirection();
}
/**
@@ -309,8 +309,8 @@ public class MonitorBlockEntity extends BlockEntity {
return isCompatible(monitor) ? MonitorState.present(monitor) : MonitorState.MISSING;
}
- private MonitorState getOrigin() {
- return getLoadedMonitor(0, 0);
+ private @Nullable MonitorBlockEntity getOrigin() {
+ return getLoadedMonitor(0, 0).getMonitor();
}
/**
@@ -389,7 +389,7 @@ public class MonitorBlockEntity extends BlockEntity {
}
void expand() {
- var monitor = getOrigin().getMonitor();
+ var monitor = getOrigin();
if (monitor != null && monitor.xIndex == 0 && monitor.yIndex == 0) new Expander(monitor).expand();
}
@@ -558,7 +558,7 @@ public class MonitorBlockEntity extends BlockEntity {
}
var hasPeripheral = false;
- var origin = getOrigin().getMonitor();
+ var origin = getOrigin();
var serverMonitor = origin != null ? origin.serverMonitor : this.serverMonitor;
for (var x = 0; x < width; x++) {
for (var y = 0; y < height; y++) {
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/speaker/DfpwmState.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/speaker/DfpwmState.java
index 2216a1156..f5988a198 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/speaker/DfpwmState.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/speaker/DfpwmState.java
@@ -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 volume) throws LuaException {
if (pendingAudio != null) return false;
@@ -45,6 +45,10 @@ class DfpwmState {
var outSize = size / 8;
var buffer = ByteBuffer.allocate(outSize);
+ var initialCharge = charge;
+ var initialStrength = strength;
+ var initialPreviousBit = previousBit;
+
for (var i = 0; i < outSize; i++) {
var thisByte = 0;
for (var j = 1; j <= 8; j++) {
@@ -80,7 +84,7 @@ class DfpwmState {
buffer.flip();
- pendingAudio = buffer;
+ pendingAudio = new EncodedAudio(initialCharge, initialStrength, initialPreviousBit, buffer);
pendingVolume = (float) clampVolume(volume.orElse((double) pendingVolume));
return true;
}
@@ -89,12 +93,12 @@ class DfpwmState {
return pendingAudio != null && now >= clientEndTime - CLIENT_BUFFER;
}
- ByteBuffer pullPending(long now) {
+ EncodedAudio pullPending(long now) {
var audio = pendingAudio;
if (audio == null) throw new IllegalStateException("Should not pull pending audio yet");
pendingAudio = null;
// Compute when we should consider sending the next packet.
- clientEndTime = Math.max(now, clientEndTime) + (audio.remaining() * SECOND * 8 / SAMPLE_RATE);
+ clientEndTime = Math.max(now, clientEndTime) + (audio.audio().remaining() * SECOND * 8 / SAMPLE_RATE);
unplayed = false;
return audio;
}
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/speaker/EncodedAudio.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/speaker/EncodedAudio.java
new file mode 100644
index 000000000..1e6ce9b5e
--- /dev/null
+++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/speaker/EncodedAudio.java
@@ -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));
+ }
+}
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/platform/PlatformHelper.java b/projects/common/src/main/java/dan200/computercraft/shared/platform/PlatformHelper.java
index b93b56dfe..2e3fb2edf 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/platform/PlatformHelper.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/platform/PlatformHelper.java
@@ -375,4 +375,13 @@ public interface PlatformHelper extends dan200.computercraft.impl.PlatformHelper
* @see ServerPlayerGameMode#useItemOn(ServerPlayer, Level, ItemStack, InteractionHand, BlockHitResult)
*/
InteractionResult useOn(ServerPlayer player, ItemStack stack, BlockHitResult hit, Predicate 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;
+ }
}
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java b/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java
index 9207dd45f..a5d75dd5e 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java
@@ -10,6 +10,7 @@ import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.shared.common.IColouredItem;
import dan200.computercraft.shared.computer.core.ComputerFamily;
+import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
@@ -38,7 +39,10 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
private ItemStack stack = ItemStack.EMPTY;
private int lightColour = -1;
- private boolean lightChanged = false;
+
+ // The state the previous tick, used to determine if the state needs to be sent to the client.
+ private int oldLightColour = -1;
+ private @Nullable ComputerState oldComputerState;
private final Set tracking = new HashSet<>();
@@ -83,10 +87,7 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
@Override
public void setLight(int colour) {
if (colour < 0 || colour > 0xFFFFFF) colour = -1;
-
- if (lightColour == colour) return;
lightColour = colour;
- lightChanged = true;
}
@Override
@@ -151,9 +152,11 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
tracking.removeIf(player -> !player.isAlive() || player.level() != getLevel());
// And now find any new players, add them to the tracking list, and broadcast state where appropriate.
- var sendState = hasOutputChanged() || lightChanged;
- lightChanged = false;
- if (sendState) {
+ var state = getState();
+ if (oldLightColour != lightColour || oldComputerState != state) {
+ oldComputerState = state;
+ oldLightColour = lightColour;
+
// Broadcast the state to all players
tracking.addAll(getLevel().players());
ServerNetworking.sendToPlayers(new PocketComputerDataMessage(this, false), tracking);
@@ -182,6 +185,6 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
@Override
protected void onRemoved() {
super.onRemoved();
- ServerNetworking.sendToAllPlayers(new PocketComputerDeletedClientMessage(getInstanceID()), getLevel().getServer());
+ ServerNetworking.sendToAllPlayers(new PocketComputerDeletedClientMessage(getInstanceUUID()), getLevel().getServer());
}
}
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItem.java b/projects/common/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItem.java
index c3e731ca3..43f0bfa4a 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItem.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItem.java
@@ -43,6 +43,7 @@ import net.minecraft.world.level.Level;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Objects;
+import java.util.UUID;
public class PocketComputerItem extends Item implements IComputerItem, IMedia, IColouredItem {
private static final String NBT_UPGRADE = "Upgrade";
@@ -188,10 +189,9 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
}
public PocketServerComputer createServerComputer(ServerLevel level, Entity entity, @Nullable Container inventory, ItemStack stack) {
- var sessionID = getSessionID(stack);
var registry = ServerContext.get(level.getServer()).registry();
- var computer = (PocketServerComputer) registry.get(sessionID, getInstanceID(stack));
+ var computer = (PocketServerComputer) registry.get(getSessionID(stack), getInstanceID(stack));
if (computer == null) {
var computerID = getComputerID(stack);
if (computerID < 0) {
@@ -201,8 +201,9 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
computer = new PocketServerComputer(level, entity.blockPosition(), getComputerID(stack), getLabel(stack), getFamily());
- setInstanceID(stack, computer.register());
- setSessionID(stack, registry.getSessionID());
+ var tag = stack.getOrCreateTag();
+ tag.putInt(NBT_SESSION, registry.getSessionID());
+ tag.putUUID(NBT_INSTANCE, computer.register());
var upgrade = getUpgrade(stack);
@@ -267,13 +268,9 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
return null;
}
- public static int getInstanceID(ItemStack stack) {
+ public static @Nullable UUID getInstanceID(ItemStack stack) {
var nbt = stack.getTag();
- return nbt != null && nbt.contains(NBT_INSTANCE) ? nbt.getInt(NBT_INSTANCE) : -1;
- }
-
- private static void setInstanceID(ItemStack stack, int instanceID) {
- stack.getOrCreateTag().putInt(NBT_INSTANCE, instanceID);
+ return nbt != null && nbt.hasUUID(NBT_INSTANCE) ? nbt.getUUID(NBT_INSTANCE) : null;
}
private static int getSessionID(ItemStack stack) {
@@ -281,10 +278,6 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
return nbt != null && nbt.contains(NBT_SESSION) ? nbt.getInt(NBT_SESSION) : -1;
}
- private static void setSessionID(ItemStack stack, int sessionID) {
- stack.getOrCreateTag().putInt(NBT_SESSION, sessionID);
- }
-
private static boolean isMarkedOn(ItemStack stack) {
var nbt = stack.getTag();
return nbt != null && nbt.getBoolean(NBT_ON);
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/blocks/TurtleBlockEntity.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/blocks/TurtleBlockEntity.java
index 68845e450..7cd3dd22e 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/blocks/TurtleBlockEntity.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/blocks/TurtleBlockEntity.java
@@ -25,10 +25,9 @@ import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.NonNullList;
import net.minecraft.nbt.CompoundTag;
-import net.minecraft.nbt.ListTag;
-import net.minecraft.nbt.Tag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.ContainerHelper;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
@@ -136,17 +135,8 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba
super.loadServer(nbt);
// Read inventory
- var nbttaglist = nbt.getList("Items", Tag.TAG_COMPOUND);
- inventory.clear();
- inventorySnapshot.clear();
- for (var i = 0; i < nbttaglist.size(); i++) {
- var tag = nbttaglist.getCompound(i);
- var slot = tag.getByte("Slot") & 0xff;
- if (slot < getContainerSize()) {
- inventory.set(slot, ItemStack.of(tag));
- inventorySnapshot.set(slot, inventory.get(slot).copy());
- }
- }
+ ContainerHelper.loadAllItems(nbt, inventory);
+ for (var i = 0; i < inventory.size(); i++) inventorySnapshot.set(i, inventory.get(i).copy());
// Read state
brain.readFromNBT(nbt);
@@ -155,16 +145,7 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba
@Override
public void saveAdditional(CompoundTag nbt) {
// Write inventory
- var nbttaglist = new ListTag();
- for (var i = 0; i < INVENTORY_SIZE; i++) {
- if (!inventory.get(i).isEmpty()) {
- var tag = new CompoundTag();
- tag.putByte("Slot", (byte) i);
- inventory.get(i).save(tag);
- nbttaglist.add(tag);
- }
- }
- nbt.put("Items", nbttaglist);
+ ContainerHelper.saveAllItems(nbt, inventory);
// Write brain
nbt = brain.writeToNBT(nbt);
@@ -186,7 +167,7 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba
if (dir.getAxis() == Direction.Axis.Y) dir = Direction.NORTH;
level.setBlockAndUpdate(worldPosition, getBlockState().setValue(TurtleBlock.FACING, dir));
- updateOutput();
+ updateRedstone();
updateInputsImmediately();
onTileEntityChange();
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java
index 70d01d941..64f484198 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java
@@ -14,6 +14,7 @@ import dan200.computercraft.api.turtle.TurtleCommand;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.core.computer.ComputerSide;
+import dan200.computercraft.core.util.PeripheralHelpers;
import dan200.computercraft.impl.TurtleUpgrades;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
@@ -296,7 +297,7 @@ public class TurtleBrain implements TurtleAccessInternal {
oldWorld.removeBlock(oldPos, false);
// Make sure everybody knows about it
- newTurtle.updateOutput();
+ newTurtle.updateRedstone();
newTurtle.updateInputsImmediately();
return true;
}
@@ -589,7 +590,7 @@ public class TurtleBrain implements TurtleAccessInternal {
}
var existing = peripherals.get(side);
- if (existing == peripheral || (existing != null && peripheral != null && existing.equals(peripheral))) {
+ if (PeripheralHelpers.equals(existing, peripheral)) {
// If the peripheral is the same, just use that.
peripheral = existing;
} else {
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleSuckCommand.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleSuckCommand.java
index 85f0bfe44..0061d9ab1 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleSuckCommand.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleSuckCommand.java
@@ -54,7 +54,7 @@ public class TurtleSuckCommand implements TurtleCommand {
case ContainerTransfer.NO_SPACE:
return TurtleCommandResult.failure("No space for items");
case ContainerTransfer.NO_ITEMS:
- return TurtleCommandResult.failure("No items to drop");
+ return TurtleCommandResult.failure("No items to take");
default:
turtle.playAnimation(TurtleAnimation.WAIT);
return TurtleCommandResult.success();
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/util/DirectionUtil.java b/projects/common/src/main/java/dan200/computercraft/shared/util/DirectionUtil.java
index 0e8b5e12f..9e691975b 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/util/DirectionUtil.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/util/DirectionUtil.java
@@ -11,6 +11,11 @@ public final class DirectionUtil {
private DirectionUtil() {
}
+ /**
+ * A bitmask indicating all sides.
+ */
+ public static final int ALL_SIDES = (1 << 6) - 1;
+
public static final Direction[] FACINGS = Direction.values();
public static ComputerSide toLocal(Direction front, Direction dir) {
@@ -31,4 +36,15 @@ public final class DirectionUtil {
default -> 0.0f;
};
}
+
+ /**
+ * Determine if a direction is in a bitmask.
+ *
+ * @param mask The bitmask to test
+ * @param direction The direction to check.
+ * @return Whether the direction is in a bitmask.
+ */
+ public static boolean isSet(int mask, Direction direction) {
+ return (mask & (1 << direction.ordinal())) != 0;
+ }
}
diff --git a/projects/common/src/main/java/dan200/computercraft/shared/util/TickScheduler.java b/projects/common/src/main/java/dan200/computercraft/shared/util/TickScheduler.java
index 7324780a7..fd459e5c1 100644
--- a/projects/common/src/main/java/dan200/computercraft/shared/util/TickScheduler.java
+++ b/projects/common/src/main/java/dan200/computercraft/shared/util/TickScheduler.java
@@ -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 toTick = new ConcurrentLinkedDeque<>();
+ /**
+ * Block entities which we want to tick, but whose chunks not currently loaded.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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> delayed = new HashMap<>();
+
+ /**
+ * Schedule a block entity to be ticked.
+ *
+ * @param token The token whose block entity should be ticked.
+ */
public static void schedule(Token token) {
var world = token.owner.getLevel();
- if (world != null && !world.isClientSide && !token.scheduled.getAndSet(true)) toTick.add(token);
+ if (world != null && !world.isClientSide && Token.STATE.compareAndSet(token, State.IDLE, State.SCHEDULED)) {
+ toTick.add(token);
+ }
+ }
+
+ public static void onChunkTicketChanged(ServerLevel level, long chunkPos, int oldLevel, int newLevel) {
+ boolean oldLoaded = isLoaded(oldLevel), newLoaded = isLoaded(newLevel);
+ if (!oldLoaded && newLoaded) {
+ // If our chunk is becoming active, requeue all pending tokens.
+ var delayedTokens = delayed.remove(new ChunkReference(level.dimension(), chunkPos));
+ if (delayedTokens == null) return;
+
+ for (var token : delayedTokens) {
+ if (token.owner.isRemoved()) {
+ Token.STATE.set(token, State.IDLE);
+ } else {
+ Token.STATE.set(token, State.SCHEDULED);
+ toTick.add(token);
+ }
+ }
+ }
+ }
+
+ public static void onChunkUnload(LevelChunk chunk) {
+ // If our chunk is fully unloaded, all block entities are about to be removed - we need to dequeue any delayed
+ // tokens from the queue.
+ var delayedTokens = delayed.remove(new ChunkReference(chunk.getLevel().dimension(), chunk.getPos().toLong()));
+ if (delayedTokens == null) return;
+
+ for (var token : delayedTokens) Token.STATE.set(token, State.IDLE);
}
public static void tick() {
Token token;
- while ((token = toTick.poll()) != null) {
- token.scheduled.set(false);
- var blockEntity = token.owner;
- if (blockEntity.isRemoved()) continue;
+ while ((token = toTick.poll()) != null) Token.STATE.set(token, tickToken(token));
+ }
- var world = blockEntity.getLevel();
- var pos = blockEntity.getBlockPos();
+ private static State tickToken(Token token) {
+ var blockEntity = token.owner;
- if (world != null && world.isLoaded(pos) && world.getBlockEntity(pos) == blockEntity) {
- world.scheduleTick(pos, blockEntity.getBlockState().getBlock(), 0);
+ // If the block entity has been removed, then remove it from the queue.
+ if (blockEntity.isRemoved()) return State.IDLE;
+
+ var level = Objects.requireNonNull(blockEntity.getLevel(), "Block entity level cannot become null");
+ var pos = blockEntity.getBlockPos();
+
+ if (!level.isLoaded(pos)) {
+ // The chunk is not properly loaded, as it to our delayed set.
+ delayed.computeIfAbsent(new ChunkReference(level.dimension(), ChunkPos.asLong(pos)), x -> new ArrayList<>()).add(token);
+ return State.UNLOADED;
+ } else {
+ // This should be impossible: either the block entity is at the above position, or it has been removed.
+ if (level.getBlockEntity(pos) != blockEntity) {
+ throw new IllegalStateException("Expected " + blockEntity + " at " + pos);
}
+
+ // Otherwise schedule a tick and remove it from the queue.
+ level.scheduleTick(pos, blockEntity.getBlockState().getBlock(), 0);
+ return State.IDLE;
}
}
@@ -52,11 +125,51 @@ public final class TickScheduler {
* As such, it should be unique per {@link BlockEntity} instance to avoid it being queued multiple times.
*/
public static class Token {
+ static final AtomicReferenceFieldUpdater 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.
+ *
+ * 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, 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);
+ }
}
diff --git a/projects/common/src/main/resources/computercraft-common.accesswidener b/projects/common/src/main/resources/computercraft-common.accesswidener
index d6e6e5044..58be6fab6 100644
--- a/projects/common/src/main/resources/computercraft-common.accesswidener
+++ b/projects/common/src/main/resources/computercraft-common.accesswidener
@@ -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;
diff --git a/projects/common/src/test/java/dan200/computercraft/client/sound/DfpwmStreamTest.java b/projects/common/src/test/java/dan200/computercraft/client/sound/DfpwmStreamTest.java
index de1f89b8e..2e7866501 100644
--- a/projects/common/src/test/java/dan200/computercraft/client/sound/DfpwmStreamTest.java
+++ b/projects/common/src/test/java/dan200/computercraft/client/sound/DfpwmStreamTest.java
@@ -4,6 +4,7 @@
package dan200.computercraft.client.sound;
+import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
import org.junit.jupiter.api.Test;
import java.nio.ByteBuffer;
@@ -16,7 +17,7 @@ public class DfpwmStreamTest {
var stream = new DfpwmStream();
var input = ByteBuffer.wrap(new byte[]{ 43, -31, 33, 44, 30, -16, -85, 23, -3, -55, 46, -70, 68, -67, 74, -96, -68, 16, 94, -87, -5, 87, 11, -16, 19, 92, 85, -71, 126, 5, -84, 64, 17, -6, 85, -11, -1, -87, -12, 1, 85, -56, 33, -80, 82, 104, -93, 17, 126, 23, 91, -30, 37, -32, 117, -72, -58, 11, -76, 19, -108, 86, -65, -10, -1, -68, -25, 10, -46, 85, 124, -54, 15, -24, 43, -94, 117, 63, -36, 15, -6, 88, 87, -26, -83, 106, 41, 13, -28, -113, -10, -66, 119, -87, -113, 68, -55, 40, -107, 62, 20, 72, 3, -96, 114, -87, -2, 39, -104, 30, 20, 42, 84, 24, 47, 64, 43, 61, -35, 95, -65, 42, 61, 42, -50, 4, -9, 81 });
- stream.push(input);
+ stream.push(new EncodedAudio(0, 0, false, input));
var buffer = stream.read(1024 + 1);
assertEquals(1024, buffer.remaining(), "Must have read 1024 bytes");
diff --git a/projects/common/src/test/java/dan200/computercraft/impl/network/wired/NetworkBenchmark.java b/projects/common/src/test/java/dan200/computercraft/impl/network/wired/NetworkBenchmark.java
new file mode 100644
index 000000000..ad3e02040
--- /dev/null
+++ b/projects/common/src/test/java/dan200/computercraft/impl/network/wired/NetworkBenchmark.java
@@ -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 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 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 countNetworks(Grid grid) {
+ Object2IntMap networks = new Object2IntOpenHashMap<>();
+ grid.forEach((node, pos) -> networks.put(node.network, networks.getOrDefault(node.network, 0) + 1));
+ return networks;
+ }
+
+ private static class Grid {
+ 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 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 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));
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/projects/common/src/test/java/dan200/computercraft/impl/network/wired/NetworkTest.java b/projects/common/src/test/java/dan200/computercraft/impl/network/wired/NetworkTest.java
index 090a5d1b1..0c76f697e 100644
--- a/projects/common/src/test/java/dan200/computercraft/impl/network/wired/NetworkTest.java
+++ b/projects/common/src/test/java/dan200/computercraft/impl/network/wired/NetworkTest.java
@@ -9,19 +9,14 @@ import dan200.computercraft.api.network.wired.WiredNetwork;
import dan200.computercraft.api.network.wired.WiredNetworkChange;
import dan200.computercraft.api.network.wired.WiredNode;
import dan200.computercraft.api.peripheral.IPeripheral;
-import dan200.computercraft.shared.util.DirectionUtil;
-import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
-import java.util.function.BiConsumer;
-import java.util.function.BiFunction;
import static org.junit.jupiter.api.Assertions.*;
@@ -29,11 +24,11 @@ public class NetworkTest {
@Test
public void testConnect() {
NetworkElement
- aE = new NetworkElement(null, null, "a"),
- bE = new NetworkElement(null, null, "b"),
- cE = new NetworkElement(null, null, "c");
+ aE = new NetworkElement("a"),
+ bE = new NetworkElement("b"),
+ cE = new NetworkElement("c");
- WiredNode
+ WiredNodeImpl
aN = aE.getNode(),
bN = bE.getNode(),
cN = cE.getNode();
@@ -42,8 +37,8 @@ public class NetworkTest {
assertNotEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be different");
assertNotEquals(bN.getNetwork(), cN.getNetwork(), "B's and C's network must be different");
- assertTrue(aN.getNetwork().connect(aN, bN), "Must be able to add connection");
- assertFalse(aN.getNetwork().connect(aN, bN), "Cannot add connection twice");
+ assertTrue(aN.connectTo(bN), "Must be able to add connection");
+ assertFalse(aN.connectTo(bN), "Cannot add connection twice");
assertEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must be equal");
assertEquals(Set.of(aN, bN), nodes(aN.getNetwork()), "A's network should be A and B");
@@ -51,7 +46,7 @@ public class NetworkTest {
assertEquals(Set.of("a", "b"), aE.allPeripherals().keySet(), "A's peripheral set should be A, B");
assertEquals(Set.of("a", "b"), bE.allPeripherals().keySet(), "B's peripheral set should be A, B");
- aN.getNetwork().connect(aN, cN);
+ aN.connectTo(cN);
assertEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must be equal");
assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal");
@@ -69,20 +64,20 @@ public class NetworkTest {
@Test
public void testDisconnectNoChange() {
NetworkElement
- aE = new NetworkElement(null, null, "a"),
- bE = new NetworkElement(null, null, "b"),
- cE = new NetworkElement(null, null, "c");
+ aE = new NetworkElement("a"),
+ bE = new NetworkElement("b"),
+ cE = new NetworkElement("c");
- WiredNode
+ WiredNodeImpl
aN = aE.getNode(),
bN = bE.getNode(),
cN = cE.getNode();
- aN.getNetwork().connect(aN, bN);
- aN.getNetwork().connect(aN, cN);
- aN.getNetwork().connect(bN, cN);
+ aN.connectTo(bN);
+ aN.connectTo(cN);
+ bN.connectTo(cN);
- aN.getNetwork().disconnect(aN, bN);
+ aN.disconnectFrom(bN);
assertEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must be equal");
assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal");
@@ -96,19 +91,19 @@ public class NetworkTest {
@Test
public void testDisconnectLeaf() {
NetworkElement
- aE = new NetworkElement(null, null, "a"),
- bE = new NetworkElement(null, null, "b"),
- cE = new NetworkElement(null, null, "c");
+ aE = new NetworkElement("a"),
+ bE = new NetworkElement("b"),
+ cE = new NetworkElement("c");
- WiredNode
+ WiredNodeImpl
aN = aE.getNode(),
bN = bE.getNode(),
cN = cE.getNode();
- aN.getNetwork().connect(aN, bN);
- aN.getNetwork().connect(aN, cN);
+ aN.connectTo(bN);
+ aN.connectTo(cN);
- aN.getNetwork().disconnect(aN, bN);
+ aN.disconnectFrom(bN);
assertNotEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal");
assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal");
@@ -123,23 +118,23 @@ public class NetworkTest {
@Test
public void testDisconnectSplit() {
NetworkElement
- aE = new NetworkElement(null, null, "a"),
- aaE = new NetworkElement(null, null, "a_"),
- bE = new NetworkElement(null, null, "b"),
- bbE = new NetworkElement(null, null, "b_");
+ aE = new NetworkElement("a"),
+ aaE = new NetworkElement("a_"),
+ bE = new NetworkElement("b"),
+ bbE = new NetworkElement("b_");
- WiredNode
+ WiredNodeImpl
aN = aE.getNode(),
aaN = aaE.getNode(),
bN = bE.getNode(),
bbN = bbE.getNode();
- aN.getNetwork().connect(aN, aaN);
- bN.getNetwork().connect(bN, bbN);
+ aN.connectTo(aaN);
+ bN.connectTo(bbN);
- aN.getNetwork().connect(aN, bN);
+ aN.connectTo(bN);
- aN.getNetwork().disconnect(aN, bN);
+ aN.disconnectFrom(bN);
assertNotEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal");
assertEquals(aN.getNetwork(), aaN.getNetwork(), "A's and A_'s network must be equal");
@@ -154,7 +149,7 @@ public class NetworkTest {
@Test
public void testRemoveSingle() {
- var aE = new NetworkElement(null, null, "a");
+ var aE = new NetworkElement("a");
var aN = aE.getNode();
var network = aN.getNetwork();
@@ -165,20 +160,20 @@ public class NetworkTest {
@Test
public void testRemoveLeaf() {
NetworkElement
- aE = new NetworkElement(null, null, "a"),
- bE = new NetworkElement(null, null, "b"),
- cE = new NetworkElement(null, null, "c");
+ aE = new NetworkElement("a"),
+ bE = new NetworkElement("b"),
+ cE = new NetworkElement("c");
- WiredNode
+ WiredNodeImpl
aN = aE.getNode(),
bN = bE.getNode(),
cN = cE.getNode();
- aN.getNetwork().connect(aN, bN);
- aN.getNetwork().connect(aN, cN);
+ aN.connectTo(bN);
+ aN.connectTo(cN);
- assertTrue(aN.getNetwork().remove(bN), "Must be able to remove node");
- assertFalse(aN.getNetwork().remove(bN), "Cannot remove a second time");
+ assertTrue(bN.remove(), "Must be able to remove node");
+ assertFalse(bN.remove(), "Cannot remove a second time");
assertNotEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal");
assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal");
@@ -194,26 +189,26 @@ public class NetworkTest {
@Test
public void testRemoveSplit() {
NetworkElement
- aE = new NetworkElement(null, null, "a"),
- aaE = new NetworkElement(null, null, "a_"),
- bE = new NetworkElement(null, null, "b"),
- bbE = new NetworkElement(null, null, "b_"),
- cE = new NetworkElement(null, null, "c");
+ aE = new NetworkElement("a"),
+ aaE = new NetworkElement("a_"),
+ bE = new NetworkElement("b"),
+ bbE = new NetworkElement("b_"),
+ cE = new NetworkElement("c");
- WiredNode
+ WiredNodeImpl
aN = aE.getNode(),
aaN = aaE.getNode(),
bN = bE.getNode(),
bbN = bbE.getNode(),
cN = cE.getNode();
- aN.getNetwork().connect(aN, aaN);
- bN.getNetwork().connect(bN, bbN);
+ aN.connectTo(aaN);
+ bN.connectTo(bbN);
- cN.getNetwork().connect(aN, cN);
- cN.getNetwork().connect(bN, cN);
+ cN.connectTo(aN);
+ cN.connectTo(bN);
- cN.getNetwork().remove(cN);
+ cN.remove();
assertNotEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal");
assertEquals(aN.getNetwork(), aaN.getNetwork(), "A's and A_'s network must be equal");
@@ -228,96 +223,30 @@ public class NetworkTest {
assertEquals(Set.of(), cE.allPeripherals().keySet(), "C's peripheral set should be empty");
}
- private static final int BRUTE_SIZE = 16;
- private static final int TOGGLE_CONNECTION_TIMES = 5;
- private static final int TOGGLE_NODE_TIMES = 5;
-
- @Test
- @Disabled("Takes a long time to run, mostly for stress testing")
- public void testLarge() {
- var grid = new Grid(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 localPeripherals = new HashMap<>();
private final Map remotePeripherals = new HashMap<>();
- private NetworkElement(Level world, Vec3 position, String id) {
- this.world = world;
- this.position = position;
+ NetworkElement(String id) {
+ this(id, true);
+ }
+
+ NetworkElement(String id, boolean peripheral) {
this.id = id;
this.node = new WiredNodeImpl(this);
- this.addPeripheral(id);
+ if (peripheral) addPeripheral(id);
}
@Override
public Level getLevel() {
- return world;
+ throw new IllegalStateException("Unexpected call to getLevel()");
}
@Override
public Vec3 getPosition() {
- return position;
+ throw new IllegalStateException("Unexpected call to getPosition()");
}
@Override
@@ -331,7 +260,7 @@ public class NetworkTest {
}
@Override
- public WiredNode getNode() {
+ public WiredNodeImpl getNode() {
return node;
}
@@ -364,45 +293,6 @@ public class NetworkTest {
}
}
- private static class Grid {
- 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 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 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 nodes(WiredNetwork network) {
return ((WiredNetworkImpl) network).nodes;
}
diff --git a/projects/common/src/test/java/dan200/computercraft/shared/command/arguments/ComputerSelectorTest.java b/projects/common/src/test/java/dan200/computercraft/shared/command/arguments/ComputerSelectorTest.java
new file mode 100644
index 000000000..a88c50d21
--- /dev/null
+++ b/projects/common/src/test/java/dan200/computercraft/shared/command/arguments/ComputerSelectorTest.java
@@ -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 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();
+ }
+}
diff --git a/projects/common/src/test/java/dan200/computercraft/shared/peripheral/speaker/DfpwmStateTest.java b/projects/common/src/test/java/dan200/computercraft/shared/peripheral/speaker/DfpwmStateTest.java
index 7d73af37e..b3cd5df53 100644
--- a/projects/common/src/test/java/dan200/computercraft/shared/peripheral/speaker/DfpwmStateTest.java
+++ b/projects/common/src/test/java/dan200/computercraft/shared/peripheral/speaker/DfpwmStateTest.java
@@ -23,7 +23,7 @@ class DfpwmStateTest {
var state = new DfpwmState();
state.pushBuffer(new ObjectLuaTable(inputTbl), input.length, Optional.empty());
- var result = state.pullPending(0);
+ var result = state.pullPending(0).audio();
var contents = new byte[result.remaining()];
result.get(contents);
diff --git a/projects/common/src/test/java/dan200/computercraft/shared/peripheral/speaker/EncodedAudioTest.java b/projects/common/src/test/java/dan200/computercraft/shared/peripheral/speaker/EncodedAudioTest.java
new file mode 100644
index 000000000..18751a82f
--- /dev/null
+++ b/projects/common/src/test/java/dan200/computercraft/shared/peripheral/speaker/EncodedAudioTest.java
@@ -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 audio() {
+ return Combinators.combine(
+ Arbitraries.integers(),
+ Arbitraries.integers(),
+ Arbitraries.of(true, false),
+ ArbitraryByteBuffer.bytes().ofMaxSize(1000)
+ ).as(EncodedAudio::new);
+ }
+}
diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Computer_Test.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Computer_Test.kt
index f7800b2b5..517580964 100644
--- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Computer_Test.kt
+++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Computer_Test.kt
@@ -11,11 +11,15 @@ import dan200.computercraft.core.apis.TermAPI
import dan200.computercraft.core.computer.ComputerSide
import dan200.computercraft.gametest.api.*
import dan200.computercraft.shared.ModRegistry
+import dan200.computercraft.test.core.assertArrayEquals
import dan200.computercraft.test.core.computer.getApi
import net.minecraft.core.BlockPos
+import net.minecraft.core.Direction
import net.minecraft.gametest.framework.GameTest
import net.minecraft.gametest.framework.GameTestHelper
import net.minecraft.world.InteractionHand
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.item.Items
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.LeverBlock
import net.minecraft.world.level.block.RedstoneLampBlock
@@ -101,6 +105,17 @@ class Computer_Test {
}
}
+ /**
+ * Check chest peripherals are reattached with a new size.
+ */
+ @GameTest
+ fun Chest_resizes_on_change(context: GameTestHelper) = context.sequence {
+ thenOnComputer { callPeripheral("right", "size").assertArrayEquals(27) }
+ thenExecute { context.placeItemAt(ItemStack(Items.CHEST), BlockPos(2, 2, 2), Direction.WEST) }
+ thenIdle(1)
+ thenOnComputer { callPeripheral("right", "size").assertArrayEquals(54) }
+ }
+
/**
* Check the client can open the computer UI and interact with it.
*/
diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Modem_Test.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Modem_Test.kt
index 738f69398..01170422f 100644
--- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Modem_Test.kt
+++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Modem_Test.kt
@@ -7,18 +7,21 @@ package dan200.computercraft.gametest
import dan200.computercraft.api.lua.ObjectArguments
import dan200.computercraft.core.apis.PeripheralAPI
import dan200.computercraft.core.computer.ComputerSide
-import dan200.computercraft.gametest.api.getBlockEntity
-import dan200.computercraft.gametest.api.sequence
-import dan200.computercraft.gametest.api.thenOnComputer
-import dan200.computercraft.gametest.api.thenStartComputer
+import dan200.computercraft.gametest.api.*
+import dan200.computercraft.impl.network.wired.WiredNodeImpl
import dan200.computercraft.shared.ModRegistry
import dan200.computercraft.shared.peripheral.modem.wired.CableBlock
+import dan200.computercraft.shared.peripheral.modem.wired.CableModemVariant
import dan200.computercraft.test.core.assertArrayEquals
import dan200.computercraft.test.core.computer.LuaTaskContext
import dan200.computercraft.test.core.computer.getApi
import net.minecraft.core.BlockPos
+import net.minecraft.core.Direction
import net.minecraft.gametest.framework.GameTest
import net.minecraft.gametest.framework.GameTestHelper
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.item.Items
+import net.minecraft.world.level.block.Blocks
import org.junit.jupiter.api.Assertions.assertEquals
import kotlin.time.Duration.Companion.milliseconds
@@ -83,7 +86,86 @@ class Modem_Test {
thenExecute {
val modem1 = helper.getBlockEntity(BlockPos(1, 2, 1), ModRegistry.BlockEntities.WIRED_MODEM_FULL.get())
val modem2 = helper.getBlockEntity(BlockPos(3, 2, 1), ModRegistry.BlockEntities.WIRED_MODEM_FULL.get())
- assertEquals(modem1.element.node.network, modem2.element.node.network, "On the same network")
+ assertEquals((modem1.element.node as WiredNodeImpl).network, (modem2.element.node as WiredNodeImpl).network, "On the same network")
+ }
+ }
+
+ /**
+ * Modems do not include the current peripheral when attached.
+ */
+ @GameTest
+ fun Cable_modem_does_not_report_self(helper: GameTestHelper) = helper.sequence {
+ // Modem does not report the computer as a peripheral.
+ thenOnComputer { assertEquals(listOf("back", "right"), getPeripheralNames()) }
+
+ // However, if we connect the network, the other modem does.
+ thenExecute {
+ helper.setBlock(
+ BlockPos(1, 2, 3),
+ ModRegistry.Blocks.CABLE.get().defaultBlockState().setValue(CableBlock.CABLE, true),
+ )
+ }
+ thenIdle(2)
+ thenOnComputer { assertEquals(listOf("back", "computer_0", "right"), getPeripheralNames()) }
+ }
+
+ /**
+ * Modems do not include the current peripheral when attached.
+ */
+ @GameTest
+ fun Full_block_modem_does_not_report_self(helper: GameTestHelper) = helper.sequence {
+ // Modem does not report the computer as a peripheral.
+ thenOnComputer { assertEquals(listOf("back", "right"), getPeripheralNames()) }
+
+ // However, if we connect the network, the other modem does.
+ thenExecute {
+ helper.setBlock(
+ BlockPos(1, 2, 3),
+ ModRegistry.Blocks.CABLE.get().defaultBlockState().setValue(CableBlock.CABLE, true),
+ )
+ }
+ thenIdle(2)
+ thenOnComputer { assertEquals(listOf("back", "computer_1", "right"), getPeripheralNames()) }
+ }
+
+ /**
+ * Test wired modems (without a cable) drop an item when the adjacent block is removed.
+ */
+ @GameTest
+ fun Modem_drops_when_neighbour_removed(helper: GameTestHelper) = helper.sequence {
+ thenExecute {
+ helper.setBlock(BlockPos(2, 3, 2), Blocks.AIR)
+ helper.assertItemEntityPresent(ModRegistry.Items.WIRED_MODEM.get(), BlockPos(2, 2, 2), 0.0)
+ helper.assertBlockPresent(Blocks.AIR, BlockPos(2, 2, 2))
+ }
+ }
+
+ /**
+ * Test wired modems (with a cable) drop an item, but keep their cable when the adjacent block is removed.
+ */
+ @GameTest
+ fun Modem_keeps_cable_when_neighbour_removed(helper: GameTestHelper) = helper.sequence {
+ thenExecute {
+ helper.setBlock(BlockPos(2, 3, 2), Blocks.AIR)
+ helper.assertItemEntityPresent(ModRegistry.Items.WIRED_MODEM.get(), BlockPos(2, 2, 2), 0.0)
+ helper.assertBlockIs(BlockPos(2, 2, 2)) {
+ it.block == ModRegistry.Blocks.CABLE.get() && it.getValue(CableBlock.MODEM) == CableModemVariant.None && it.getValue(CableBlock.CABLE)
+ }
+ }
+ }
+
+ /**
+ * Check chest peripherals are reattached with a new size.
+ */
+ @GameTest
+ fun Chest_resizes_on_change(context: GameTestHelper) = context.sequence {
+ thenOnComputer {
+ callRemotePeripheral("minecraft:chest_0", "size").assertArrayEquals(27)
+ }
+ thenExecute { context.placeItemAt(ItemStack(Items.CHEST), BlockPos(2, 2, 2), Direction.WEST) }
+ thenIdle(1)
+ thenOnComputer {
+ callRemotePeripheral("minecraft:chest_0", "size").assertArrayEquals(54)
}
}
}
@@ -105,7 +187,7 @@ private suspend fun LuaTaskContext.getPeripheralNames(): List {
if (!peripheral.isPresent(side)) continue
peripherals.add(side)
- val hasType = peripheral.hasType(side, "modem")
+ val hasType = peripheral.hasType(side, "peripheral_hub")
if (hasType == null || hasType[0] != true) continue
val names = peripheral.call(context, ObjectArguments(side, "getNamesRemote")).await() ?: continue
@@ -116,3 +198,22 @@ private suspend fun LuaTaskContext.getPeripheralNames(): List {
peripherals.sort()
return peripherals
}
+
+private suspend fun LuaTaskContext.callRemotePeripheral(name: String, method: String, vararg args: Any): Array? {
+ val peripheral = getApi()
+ 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")
+}
diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Pocket_Computer_Test.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Pocket_Computer_Test.kt
index d070f8e0f..d45844039 100644
--- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Pocket_Computer_Test.kt
+++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Pocket_Computer_Test.kt
@@ -38,7 +38,7 @@ class Pocket_Computer_Test {
// And ensure its synced to the client.
thenIdle(4)
thenOnClient {
- val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)
+ val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)!!
assertEquals(ComputerState.ON, pocketComputer.state)
val term = pocketComputer.terminal
@@ -54,7 +54,7 @@ class Pocket_Computer_Test {
// And ensure the new computer state and terminal are sent.
thenIdle(4)
thenOnClient {
- val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)
+ val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)!!
assertEquals(ComputerState.BLINKING, pocketComputer.state)
val term = pocketComputer.terminal
diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt
index 03f998947..40ed67cf4 100644
--- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt
+++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt
@@ -329,8 +329,6 @@ class Turtle_Test {
/**
* Checks turtles can be cleaned in cauldrons.
- *
- * Currently not required as turtles can no longer right-click cauldrons.
*/
@GameTest
fun Cleaned_with_cauldrons(helper: GameTestHelper) = helper.sequence {
@@ -643,7 +641,20 @@ class Turtle_Test {
}
}
- // TODO: Turtle sucking from items
+ /**
+ * `turtle.suck` only pulls for the current side.
+ */
+ @GameTest
+ fun Sided_suck(helper: GameTestHelper) = helper.sequence {
+ thenOnComputer {
+ turtle.suckUp(Optional.empty()).await().assertArrayEquals(true)
+ turtle.getItemDetail(context, Optional.empty(), Optional.empty()).await().assertArrayEquals(
+ mapOf("name" to "minecraft:iron_ingot", "count" to 8),
+ )
+
+ turtle.suckUp(Optional.empty()).await().assertArrayEquals(false, "No items to take")
+ }
+ }
/**
* Render turtles as an item.
diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/api/TestExtensions.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/api/TestExtensions.kt
index 60921532a..772bf9c36 100644
--- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/api/TestExtensions.kt
+++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/api/TestExtensions.kt
@@ -20,15 +20,19 @@ import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.gametest.framework.*
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.Container
+import net.minecraft.world.InteractionHand
import net.minecraft.world.entity.Entity
import net.minecraft.world.entity.EntityType
import net.minecraft.world.item.ItemStack
+import net.minecraft.world.item.context.UseOnContext
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.entity.BarrelBlockEntity
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.entity.BlockEntityType
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.properties.Property
+import net.minecraft.world.phys.BlockHitResult
+import net.minecraft.world.phys.Vec3
import org.hamcrest.Matchers
import org.hamcrest.StringDescription
@@ -306,3 +310,16 @@ fun GameTestHelper.setContainerItem(pos: BlockPos, slot: Int, item: ItemStack) {
container.setItem(slot, item)
container.setChanged()
}
+
+/**
+ * An alternative version ot [GameTestHelper.placeAt], which sets the player's held item first.
+ *
+ * This is required for compatibility with Forge, which uses the in-hand stack, rather than the stack requested.
+ */
+fun GameTestHelper.placeItemAt(stack: ItemStack, pos: BlockPos, direction: Direction) {
+ val player = makeMockPlayer()
+ player.setItemInHand(InteractionHand.MAIN_HAND, stack)
+ val absolutePos = absolutePos(pos.relative(direction))
+ val hit = BlockHitResult(Vec3.atCenterOf(absolutePos), direction, absolutePos, false)
+ stack.useOn(UseOnContext(player, InteractionHand.MAIN_HAND, hit))
+}
diff --git a/projects/common/src/testMod/resources/data/cctest/structures/computer_test.chest_resizes_on_change.snbt b/projects/common/src/testMod/resources/data/cctest/structures/computer_test.chest_resizes_on_change.snbt
new file mode 100644
index 000000000..877e24bdd
--- /dev/null
+++ b/projects/common/src/testMod/resources/data/cctest/structures/computer_test.chest_resizes_on_change.snbt
@@ -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}"
+ ]
+}
diff --git a/projects/common/src/testMod/resources/data/cctest/structures/modem_test.cable_modem_does_not_report_self.snbt b/projects/common/src/testMod/resources/data/cctest/structures/modem_test.cable_modem_does_not_report_self.snbt
new file mode 100644
index 000000000..9d37cf778
--- /dev/null
+++ b/projects/common/src/testMod/resources/data/cctest/structures/modem_test.cable_modem_does_not_report_self.snbt
@@ -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}"
+ ]
+}
diff --git a/projects/common/src/testMod/resources/data/cctest/structures/modem_test.chest_resizes_on_change.snbt b/projects/common/src/testMod/resources/data/cctest/structures/modem_test.chest_resizes_on_change.snbt
new file mode 100644
index 000000000..b5fbf4cf8
--- /dev/null
+++ b/projects/common/src/testMod/resources/data/cctest/structures/modem_test.chest_resizes_on_change.snbt
@@ -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}"
+ ]
+}
diff --git a/projects/common/src/testMod/resources/data/cctest/structures/modem_test.full_block_modem_does_not_report_self.snbt b/projects/common/src/testMod/resources/data/cctest/structures/modem_test.full_block_modem_does_not_report_self.snbt
new file mode 100644
index 000000000..3c8aa88a0
--- /dev/null
+++ b/projects/common/src/testMod/resources/data/cctest/structures/modem_test.full_block_modem_does_not_report_self.snbt
@@ -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}"
+ ]
+}
diff --git a/projects/common/src/testMod/resources/data/cctest/structures/modem_test.gains_peripherals.snbt b/projects/common/src/testMod/resources/data/cctest/structures/modem_test.gains_peripherals.snbt
index fa5f338ad..afb1e3ad7 100644
--- a/projects/common/src/testMod/resources/data/cctest/structures/modem_test.gains_peripherals.snbt
+++ b/projects/common/src/testMod/resources/data/cctest/structures/modem_test.gains_peripherals.snbt
@@ -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}"
diff --git a/projects/common/src/testMod/resources/data/cctest/structures/modem_test.have_peripherals.snbt b/projects/common/src/testMod/resources/data/cctest/structures/modem_test.have_peripherals.snbt
index 65395414d..ffbb2819a 100644
--- a/projects/common/src/testMod/resources/data/cctest/structures/modem_test.have_peripherals.snbt
+++ b/projects/common/src/testMod/resources/data/cctest/structures/modem_test.have_peripherals.snbt
@@ -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}"
diff --git a/projects/common/src/testMod/resources/data/cctest/structures/modem_test.modem_drops_when_neighbour_removed.snbt b/projects/common/src/testMod/resources/data/cctest/structures/modem_test.modem_drops_when_neighbour_removed.snbt
new file mode 100644
index 000000000..3007188a3
--- /dev/null
+++ b/projects/common/src/testMod/resources/data/cctest/structures/modem_test.modem_drops_when_neighbour_removed.snbt
@@ -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}"
+ ]
+}
diff --git a/projects/common/src/testMod/resources/data/cctest/structures/modem_test.modem_keeps_cable_when_neighbour_removed.snbt b/projects/common/src/testMod/resources/data/cctest/structures/modem_test.modem_keeps_cable_when_neighbour_removed.snbt
new file mode 100644
index 000000000..be462dd2c
--- /dev/null
+++ b/projects/common/src/testMod/resources/data/cctest/structures/modem_test.modem_keeps_cable_when_neighbour_removed.snbt
@@ -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}"
+ ]
+}
diff --git a/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.sided_suck.snbt b/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.sided_suck.snbt
new file mode 100644
index 000000000..b520ce1cc
--- /dev/null
+++ b/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.sided_suck.snbt
@@ -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}"
+ ]
+}
diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/lua/MethodResult.java b/projects/core-api/src/main/java/dan200/computercraft/api/lua/MethodResult.java
index 712277098..aec0f8615 100644
--- a/projects/core-api/src/main/java/dan200/computercraft/api/lua/MethodResult.java
+++ b/projects/core-api/src/main/java/dan200/computercraft/api/lua/MethodResult.java
@@ -116,6 +116,7 @@ public final class MethodResult {
* @return The method result which represents this yield.
* @see #pullEvent(String, ILuaCallback)
*/
+ @SuppressWarnings("NamedLikeContextualKeyword")
public static MethodResult yield(@Nullable Object[] arguments, ILuaCallback callback) {
Objects.requireNonNull(callback, "callback cannot be null");
return new MethodResult(arguments, callback);
diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java b/projects/core/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java
index 70fdb0b1c..0b5307fcd 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java
@@ -11,6 +11,7 @@ import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.peripheral.NotAttachedException;
import dan200.computercraft.api.peripheral.WorkMonitor;
import dan200.computercraft.core.computer.ComputerSide;
+import dan200.computercraft.core.computer.GuardedLuaContext;
import dan200.computercraft.core.methods.MethodSupplier;
import dan200.computercraft.core.methods.PeripheralMethod;
import dan200.computercraft.core.metrics.Metrics;
@@ -26,7 +27,7 @@ import java.util.*;
* @hidden
*/
public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChangeListener {
- private class PeripheralWrapper extends ComputerAccess {
+ private class PeripheralWrapper extends ComputerAccess implements GuardedLuaContext.Guard {
private final String side;
private final IPeripheral peripheral;
@@ -35,6 +36,8 @@ public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange
private final Map methodMap;
private boolean attached = false;
+ private @Nullable GuardedLuaContext contextWrapper;
+
PeripheralWrapper(IPeripheral peripheral, String side) {
super(environment);
this.side = side;
@@ -91,9 +94,21 @@ public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange
if (method == null) throw new LuaException("No such method " + methodName);
- try (var ignored = environment.time(Metrics.PERIPHERAL_OPS)) {
- return method.apply(peripheral, context, this, arguments);
+ // Wrap the ILuaContext. We try to reuse the previous context where possible to avoid allocations - this
+ // should be pretty common as ILuaMachine uses a constant context.
+ var contextWrapper = this.contextWrapper;
+ if (contextWrapper == null || !contextWrapper.wraps(context)) {
+ contextWrapper = this.contextWrapper = new GuardedLuaContext(context, this);
}
+
+ try (var ignored = environment.time(Metrics.PERIPHERAL_OPS)) {
+ return method.apply(peripheral, contextWrapper, this, arguments);
+ }
+ }
+
+ @Override
+ public boolean checkValid() {
+ return isAttached();
}
// IComputerAccess implementation
diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/TermAPI.java b/projects/core/src/main/java/dan200/computercraft/core/apis/TermAPI.java
index 3da4cd85f..cba5342f0 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/apis/TermAPI.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/apis/TermAPI.java
@@ -13,8 +13,54 @@ import dan200.computercraft.core.util.Colour;
/**
- * Interact with a computer's terminal or monitors, writing text and drawing
- * ASCII graphics.
+ * Interact with a computer's terminal or monitors, writing text and drawing ASCII graphics.
+ *
+ * Writing to the terminal
+ * The simplest operation one can perform on a terminal is displaying (or writing) some text. This can be performed with
+ * the [`term.write`] method.
+ *
+ * {@code
+ * term.write("Hello, world!")
+ * }
+ *
+ * When you write text, this advances the cursor, so the next call to [`term.write`] will write text immediately after
+ * the previous one.
+ *
+ *
{@code
+ * term.write("Hello, world!")
+ * term.write("Some more text")
+ * }
+ *
+ * [`term.getCursorPos`] and [`term.setCursorPos`] can be used to manually change the cursor's position.
+ *
+ *
{@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")
+ * }
+ *
+ * [`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.
+ *
+ *
Colours
+ * 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.
+ *
+ * {@code
+ * print("This text is white")
+ * term.setTextColour(colours.green)
+ * print("This text is green")
+ * }
+ *
+ * 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`]).
+ *
+ * The [`paintutils`] API provides several helpful functions for displaying graphics using [`term.setBackgroundColour`].
*
* @cc.module term
*/
diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/Options.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/Options.java
index ef408e4c3..493f1b39e 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/Options.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/Options.java
@@ -4,6 +4,7 @@
package dan200.computercraft.core.apis.http.options;
+import com.google.errorprone.annotations.Immutable;
/**
* Options for a given HTTP request or websocket, which control its resource constraints.
@@ -14,5 +15,6 @@ package dan200.computercraft.core.apis.http.options;
* @param websocketMessage The maximum size of a websocket message (outgoing and incoming).
* @param useProxy Whether to use the configured proxy.
*/
+@Immutable
public record Options(Action action, long maxUpload, long maxDownload, int websocketMessage, boolean useProxy) {
}
diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/PartialOptions.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/PartialOptions.java
index 280977872..bd0c91c6f 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/PartialOptions.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/PartialOptions.java
@@ -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 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 useProxy) {
diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/ResultHelpers.java b/projects/core/src/main/java/dan200/computercraft/core/asm/ResultHelpers.java
index f56d1246e..ef2e55a80 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/asm/ResultHelpers.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/asm/ResultHelpers.java
@@ -27,7 +27,7 @@ final class ResultHelpers {
return throwUnchecked0(t);
}
- @SuppressWarnings("unchecked")
+ @SuppressWarnings({ "unchecked", "TypeParameterUnusedInFormals" })
private static T throwUnchecked0(Throwable t) throws T {
throw (T) t;
}
diff --git a/projects/core/src/main/java/dan200/computercraft/core/computer/Computer.java b/projects/core/src/main/java/dan200/computercraft/core/computer/Computer.java
index fb285a67e..b2d4f1e13 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/computer/Computer.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/computer/Computer.java
@@ -15,8 +15,7 @@ import dan200.computercraft.core.filesystem.FileSystem;
import dan200.computercraft.core.terminal.Terminal;
import javax.annotation.Nullable;
-import java.util.Objects;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
/**
@@ -54,9 +53,9 @@ public class Computer {
private final AtomicLong lastTaskId = new AtomicLong();
// Additional state about the computer and its environment.
- private boolean blinking = false;
private final Environment internalEnvironment;
- private final AtomicBoolean externalOutputChanged = new AtomicBoolean();
+
+ private final AtomicInteger externalOutputChanges = new AtomicInteger();
private boolean startRequested;
private int ticksSinceStart = -1;
@@ -140,10 +139,7 @@ public class Computer {
}
public void setLabel(@Nullable String label) {
- if (!Objects.equals(label, this.label)) {
- this.label = label;
- externalOutputChanged.set(true);
- }
+ this.label = label;
}
public void tick() {
@@ -164,28 +160,24 @@ public class Computer {
internalEnvironment.tick();
// Propagate the environment's output to the world.
- if (internalEnvironment.updateOutput()) externalOutputChanged.set(true);
-
- // Set output changed if the terminal has changed from blinking to not
- var blinking = terminal.getCursorBlink() &&
- terminal.getCursorX() >= 0 && terminal.getCursorX() < terminal.getWidth() &&
- terminal.getCursorY() >= 0 && terminal.getCursorY() < terminal.getHeight();
- if (blinking != this.blinking) {
- this.blinking = blinking;
- externalOutputChanged.set(true);
- }
+ externalOutputChanges.accumulateAndGet(internalEnvironment.updateOutput(), (x, y) -> x | y);
}
- void markChanged() {
- externalOutputChanged.set(true);
- }
-
- public boolean pollAndResetChanged() {
- return externalOutputChanged.getAndSet(false);
+ /**
+ * Get a bitmask returning which sides on the computer have changed, resetting the internal state.
+ *
+ * @return What sides on the computer have changed.
+ */
+ public int pollAndResetChanges() {
+ return externalOutputChanges.getAndSet(0);
}
public boolean isBlinking() {
- return isOn() && blinking;
+ if (!isOn() || !terminal.getCursorBlink()) return false;
+
+ var cursorX = terminal.getCursorX();
+ var cursorY = terminal.getCursorY();
+ return cursorX >= 0 && cursorX < terminal.getWidth() && cursorY >= 0 && cursorY < terminal.getHeight();
}
public void addApi(ILuaAPI api) {
diff --git a/projects/core/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java b/projects/core/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java
index 73ac27f1c..3bb1c551e 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java
@@ -411,7 +411,6 @@ final class ComputerExecutor implements ComputerScheduler.Worker {
// Initialisation has finished, so let's mark ourselves as on.
isOn = true;
- computer.markChanged();
} finally {
isOnLock.unlock();
}
@@ -446,7 +445,6 @@ final class ComputerExecutor implements ComputerScheduler.Worker {
}
computer.getEnvironment().resetOutput();
- computer.markChanged();
} finally {
isOnLock.unlock();
}
diff --git a/projects/core/src/main/java/dan200/computercraft/core/computer/Environment.java b/projects/core/src/main/java/dan200/computercraft/core/computer/Environment.java
index d6bb1ff4e..15d38bfa1 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/computer/Environment.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/computer/Environment.java
@@ -11,6 +11,7 @@ import dan200.computercraft.core.apis.IAPIEnvironment;
import dan200.computercraft.core.filesystem.FileSystem;
import dan200.computercraft.core.metrics.MetricsObserver;
import dan200.computercraft.core.terminal.Terminal;
+import dan200.computercraft.core.util.PeripheralHelpers;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
@@ -222,22 +223,22 @@ public final class Environment implements IAPIEnvironment {
*
* @return If the outputs have changed.
*/
- boolean updateOutput() {
+ int updateOutput() {
// Mark output as changed if the internal redstone has changed
synchronized (internalOutput) {
- if (!internalOutputChanged) return false;
+ if (!internalOutputChanged) return 0;
- var changed = false;
+ var changed = 0;
for (var i = 0; i < ComputerSide.COUNT; i++) {
if (externalOutput[i] != internalOutput[i]) {
externalOutput[i] = internalOutput[i];
- changed = true;
+ changed |= 1 << i;
}
if (externalBundledOutput[i] != internalBundledOutput[i]) {
externalBundledOutput[i] = internalBundledOutput[i];
- changed = true;
+ changed |= 1 << i;
}
}
@@ -268,9 +269,7 @@ public final class Environment implements IAPIEnvironment {
synchronized (peripherals) {
var index = side.ordinal();
var existing = peripherals[index];
- if ((existing == null && peripheral != null) ||
- (existing != null && peripheral == null) ||
- (existing != null && !existing.equals(peripheral))) {
+ if (!PeripheralHelpers.equals(existing, peripheral)) {
peripherals[index] = peripheral;
if (peripheralListener != null) peripheralListener.onPeripheralChanged(side, peripheral);
}
diff --git a/projects/core/src/main/java/dan200/computercraft/core/computer/GuardedLuaContext.java b/projects/core/src/main/java/dan200/computercraft/core/computer/GuardedLuaContext.java
new file mode 100644
index 000000000..06dc65235
--- /dev/null
+++ b/projects/core/src/main/java/dan200/computercraft/core/computer/GuardedLuaContext.java
@@ -0,0 +1,55 @@
+// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
+//
+// SPDX-License-Identifier: MPL-2.0
+
+package dan200.computercraft.core.computer;
+
+import dan200.computercraft.api.lua.ILuaContext;
+import dan200.computercraft.api.lua.LuaException;
+import dan200.computercraft.api.lua.LuaTask;
+
+/**
+ * A {@link ILuaContext} which checks if context is valid when before executing
+ * {@linkplain #issueMainThreadTask(LuaTask) main-thread tasks}.
+ */
+public final class GuardedLuaContext implements ILuaContext {
+ private final ILuaContext original;
+ private final Guard guard;
+
+ public GuardedLuaContext(ILuaContext original, Guard guard) {
+ this.original = original;
+ this.guard = guard;
+ }
+
+ /**
+ * Determine if this {@link GuardedLuaContext} wraps another context.
+ *
+ * This may be used to avoid constructing new guarded contexts, in a pattern something like:
+ *
+ *
{@code
+ * var contextWrapper = this.contextWrapper;
+ * if(contextWrapper == null || !contextWrapper.wraps(context)) {
+ * contextWrapper = this.contextWrapper = new GuardedLuaContext(context, this);
+ * }
+ * }
+ *
+ * @param context The original context.
+ * @return Whether {@code this} wraps {@code context}.
+ */
+ public boolean wraps(ILuaContext context) {
+ return original == context;
+ }
+
+ @Override
+ public long issueMainThreadTask(LuaTask task) throws LuaException {
+ return original.issueMainThreadTask(() -> guard.checkValid() ? task.execute() : null);
+ }
+
+ /**
+ * The function which checks if the context is still valid.
+ */
+ @FunctionalInterface
+ public interface Guard {
+ boolean checkValid();
+ }
+}
diff --git a/projects/core/src/main/java/dan200/computercraft/core/computer/computerthread/ComputerThread.java b/projects/core/src/main/java/dan200/computercraft/core/computer/computerthread/ComputerThread.java
index 5ea13b946..6b109d7be 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/computer/computerthread/ComputerThread.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/computer/computerthread/ComputerThread.java
@@ -518,6 +518,7 @@ public final class ComputerThread implements ComputerScheduler {
} else {
allocations = null;
}
+ var allocationTime = System.nanoTime();
for (var i = 0; i < workers.length; i++) {
var runner = workers[i];
@@ -532,7 +533,7 @@ public final class ComputerThread implements ComputerScheduler {
// And track the allocated memory.
if (allocations != null) {
- executor.updateAllocations(new ThreadAllocation(workerThreadIds[i], allocations[i]));
+ executor.updateAllocations(new ThreadAllocation(workerThreadIds[i], allocations[i], allocationTime));
}
// If we're still within normal execution times (TIMEOUT) or soft abort (ABORT_TIMEOUT),
@@ -814,7 +815,7 @@ public final class ComputerThread implements ComputerScheduler {
if (ThreadAllocations.isSupported()) {
var current = Thread.currentThread().getId();
- THREAD_ALLOCATION.set(this, new ThreadAllocation(current, ThreadAllocations.getAllocatedBytes(current)));
+ THREAD_ALLOCATION.set(this, new ThreadAllocation(current, ThreadAllocations.getAllocatedBytes(current), System.nanoTime()));
}
}
@@ -832,11 +833,20 @@ public final class ComputerThread implements ComputerScheduler {
var info = THREAD_ALLOCATION.getAndSet(this, null);
assert info.threadId() == current;
- var allocated = ThreadAllocations.getAllocatedBytes(current) - info.allocatedBytes();
+ var allocatedTotal = ThreadAllocations.getAllocatedBytes(current);
+ var allocated = allocatedTotal - info.allocatedBytes();
if (allocated > 0) {
metrics.observe(Metrics.JAVA_ALLOCATION, allocated);
} else if (allocated < 0) {
- LOG.warn("Allocated a negative number of bytes ({})!", allocated);
+ LOG.warn(
+ """
+ Allocated a negative number of bytes ({})!
+ Previous measurement at t={} on Thread #{} = {}
+ Current measurement at t={} on Thread #{} = {}""",
+ allocated,
+ info.time(), info.threadId(), info.allocatedBytes(),
+ System.nanoTime(), current, allocatedTotal
+ );
}
}
@@ -901,7 +911,8 @@ public final class ComputerThread implements ComputerScheduler {
*
* @param threadId The ID of this thread.
* @param allocatedBytes The amount of memory this thread has allocated.
+ * @param time The time (in nanoseconds) when this time was computed.
*/
- private record ThreadAllocation(long threadId, long allocatedBytes) {
+ private record ThreadAllocation(long threadId, long allocatedBytes, long time) {
}
}
diff --git a/projects/core/src/main/java/dan200/computercraft/core/util/PeripheralHelpers.java b/projects/core/src/main/java/dan200/computercraft/core/util/PeripheralHelpers.java
new file mode 100644
index 000000000..6ac48da4e
--- /dev/null
+++ b/projects/core/src/main/java/dan200/computercraft/core/util/PeripheralHelpers.java
@@ -0,0 +1,29 @@
+// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
+//
+// SPDX-License-Identifier: MPL-2.0
+
+package dan200.computercraft.core.util;
+
+import dan200.computercraft.api.peripheral.IPeripheral;
+
+import javax.annotation.Nullable;
+
+/**
+ * Utilities for working with {@linkplain IPeripheral peripherals}.
+ */
+public final class PeripheralHelpers {
+ private PeripheralHelpers() {
+ }
+
+ /**
+ * Determine if two peripherals are equal. This is equivalent to {@link java.util.Objects#equals(Object, Object)},
+ * but using {@link IPeripheral#equals(IPeripheral)} instead.
+ *
+ * @param a The first peripheral.
+ * @param b The second peripheral.
+ * @return If the two peripherals are equal.
+ */
+ public static boolean equals(@Nullable IPeripheral a, @Nullable IPeripheral b) {
+ return a == b || (a != null && b != null && a.equals(b));
+ }
+}
diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/colors.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/colors.lua
index 70ab61784..b98ff8d75 100644
--- a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/colors.lua
+++ b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/colors.lua
@@ -353,7 +353,8 @@ end
--[[- Converts the given color to a paint/blit hex character (0-9a-f).
-This is equivalent to converting floor(log_2(color)) to hexadecimal.
+This is equivalent to converting `floor(log_2(color))` to hexadecimal. Values
+outside the range of a valid colour will error.
@tparam number color The color to convert.
@treturn string The blit hex code of the color.
@@ -367,7 +368,11 @@ colors.toBlit(colors.red)
]]
function toBlit(color)
expect(1, color, "number")
- return color_hex_lookup[color] or string.format("%x", math.floor(math.log(color, 2)))
+ local hex = color_hex_lookup[color]
+ if hex then return hex end
+
+ if color < 0 or color > 0xffff then error("Colour out of range", 2) end
+ return string.format("%x", math.floor(math.log(color, 2)))
end
--[[- Converts the given paint/blit hex character (0-9a-f) to a color.
diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua
index 848ccfb91..7be7aebaf 100644
--- a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua
+++ b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua
@@ -58,6 +58,17 @@ local type = type
local string_rep = string.rep
local string_sub = string.sub
+--- A custom version of [`colors.toBlit`], specialised for the window API.
+local function parse_color(color)
+ if type(color) ~= "number" then
+ -- By tail-calling expect, we ensure expect has the right error level.
+ return expect(1, color, "number")
+ end
+
+ if color < 0 or color > 0xffff then error("Colour out of range", 3) end
+ return 2 ^ math.floor(math.log(color, 2))
+end
+
--[[- Returns a terminal object that is a space within the specified parent
terminal object. This can then be used (or even redirected to) in the same
manner as eg a wrapped monitor. Refer to [the term API][`term`] for a list of
@@ -341,10 +352,7 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
end
local function setTextColor(color)
- if type(color) ~= "number" then expect(1, color, "number") end
- if tHex[color] == nil then
- error("Invalid color (got " .. color .. ")" , 2)
- end
+ if tHex[color] == nil then color = parse_color(color) end
nTextColor = color
if bVisible then
@@ -356,11 +364,7 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
window.setTextColour = setTextColor
function window.setPaletteColour(colour, r, g, b)
- if type(colour) ~= "number" then expect(1, colour, "number") end
-
- if tHex[colour] == nil then
- error("Invalid color (got " .. colour .. ")" , 2)
- end
+ if tHex[colour] == nil then colour = parse_color(colour) end
local tCol
if type(r) == "number" and g == nil and b == nil then
@@ -385,10 +389,7 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
window.setPaletteColor = window.setPaletteColour
function window.getPaletteColour(colour)
- if type(colour) ~= "number" then expect(1, colour, "number") end
- if tHex[colour] == nil then
- error("Invalid color (got " .. colour .. ")" , 2)
- end
+ if tHex[colour] == nil then colour = parse_color(colour) end
local tCol = tPalette[colour]
return tCol[1], tCol[2], tCol[3]
end
@@ -396,10 +397,7 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
window.getPaletteColor = window.getPaletteColour
local function setBackgroundColor(color)
- if type(color) ~= "number" then expect(1, color, "number") end
- if tHex[color] == nil then
- error("Invalid color (got " .. color .. ")", 2)
- end
+ if tHex[color] == nil then color = parse_color(color) end
nBackgroundColor = color
end
diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/help/changelog.md b/projects/core/src/main/resources/data/computercraft/lua/rom/help/changelog.md
index d728ba46a..9cb7717a4 100644
--- a/projects/core/src/main/resources/data/computercraft/lua/rom/help/changelog.md
+++ b/projects/core/src/main/resources/data/computercraft/lua/rom/help/changelog.md
@@ -1,3 +1,28 @@
+# New features in CC: Tweaked 1.110.0
+
+* Add a new `@c[...]` syntax for selecting computers in the `/computercraft` command.
+* Remove custom breaking progress of modems on Forge.
+
+Several bug fixes:
+* Fix client and server DFPWM transcoders getting out of sync.
+* Fix `turtle.suck` reporting incorrect error when failing to suck items.
+* Fix pocket computers displaying state (blinking, modem light) for the wrong computer.
+* Fix crash when wrapping an invalid BE as a generic peripheral.
+* Chest peripherals now reattach when a chest is converted into a double chest.
+* Fix `speaker` program not resolving files relative to the current directory.
+* Skip main-thread tasks if the peripheral is detached.
+* Fix internal Lua VM errors if yielding inside `__tostring`.
+
+# New features in CC: Tweaked 1.109.7
+
+* Improve performance of removing and unloading wired cables/modems.
+
+Several bug fixes:
+* Fix monitors sometimes not updating on the client when chunks are unloaded and reloaded.
+* `colour.toBlit` correctly errors on out-of-bounds values.
+* Round non-standard colours in `window`, like `term.native()` does.
+* Fix the client monitor rendering both the current and outdated contents.
+
# New features in CC: Tweaked 1.109.6
* Improve several Lua parser error messages.
diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md b/projects/core/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md
index 4a57b064e..4afa55617 100644
--- a/projects/core/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md
+++ b/projects/core/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md
@@ -1,9 +1,16 @@
-New features in CC: Tweaked 1.109.6
+New features in CC: Tweaked 1.110.0
-* Improve several Lua parser error messages.
-* Allow addon mods to register `require`able modules.
+* Add a new `@c[...]` syntax for selecting computers in the `/computercraft` command.
+* Remove custom breaking progress of modems on Forge.
Several bug fixes:
-* Fix weak tables becoming malformed when keys are GCed.
+* Fix client and server DFPWM transcoders getting out of sync.
+* Fix `turtle.suck` reporting incorrect error when failing to suck items.
+* Fix pocket computers displaying state (blinking, modem light) for the wrong computer.
+* Fix crash when wrapping an invalid BE as a generic peripheral.
+* Chest peripherals now reattach when a chest is converted into a double chest.
+* Fix `speaker` program not resolving files relative to the current directory.
+* Skip main-thread tasks if the peripheral is detached.
+* Fix internal Lua VM errors if yielding inside `__tostring`.
Type "help changelog" to see the full version history.
diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/exception.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/exception.lua
index 64156934f..7f22cd95c 100644
--- a/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/exception.lua
+++ b/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/exception.lua
@@ -26,13 +26,51 @@ local function find_frame(thread, file, line)
end
end
+--[[- Check whether this error is an exception.
+
+Currently we don't provide a stable API for throwing (and propogating) rich
+errors, like those supported by this module. In lieu of that, we describe the
+exception protocol, which may be used by user-written coroutine managers to
+throw exceptions which are pretty-printed by the shell:
+
+An exception is any table with:
+ - The `"exception"` type
+ - A string `message` field,
+ - And a coroutine `thread` fields.
+
+To throw such an exception, the inner loop of your coroutine manager may look
+something like this:
+
+```lua
+local ok, result = coroutine.resume(co, table.unpack(event, 1, event.n))
+if not ok then
+ -- Rethrow non-string errors directly
+ if type(result) ~= "string" then error(result, 0) end
+ -- Otherwise, wrap it into an exception.
+ error(setmetatable({ message = result, thread = co }, {
+ __name = "exception",
+ __tostring = function(self) return self.message end,
+ }))
+end
+```
+
+@param exn Some error object
+@treturn boolean Whether this error is an exception.
+]]
+local function is_exception(exn)
+ if type(exn) ~= "table" then return false end
+
+ local mt = getmetatable(exn)
+ return mt and mt.__name == "exception" and type(rawget(exn, "message")) == "string" and type(rawget(exn, "thread")) == "thread"
+end
+
--[[- Attempt to call the provided function `func` with the provided arguments.
@tparam function func The function to call.
@param ... Arguments to this function.
@treturn[1] true If the function ran successfully.
- @return[1] ... The return values of the function.
+@return[1] ... The return values of the function.
@treturn[2] false If the function failed.
@return[2] The error message
@@ -51,8 +89,14 @@ local function try(func, ...)
end
end
- if not result[1] then return false, result[2], co end
- return table.unpack(result, 1, result.n)
+ if result[1] then
+ return table.unpack(result, 1, result.n)
+ elseif is_exception(result[2]) then
+ local exn = result[2]
+ return false, rawget(exn, "message"), rawget(exn, "thread")
+ else
+ return false, result[2], co
+ end
end
--[[- Report additional context about an error.
diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/fun/speaker.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/fun/speaker.lua
index a26565c97..883b7eaff 100644
--- a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/fun/speaker.lua
+++ b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/fun/speaker.lua
@@ -50,7 +50,7 @@ elseif cmd == "play" then
print("Downloading...")
handle, err = http.get(file)
else
- handle, err = fs.open(file, "r")
+ handle, err = fs.open(shell.resolve(file), "r")
end
if not handle then
diff --git a/projects/core/src/test/java/dan200/computercraft/core/LuaCoverage.java b/projects/core/src/test/java/dan200/computercraft/core/LuaCoverage.java
index e7306c780..29392a83c 100644
--- a/projects/core/src/test/java/dan200/computercraft/core/LuaCoverage.java
+++ b/projects/core/src/test/java/dan200/computercraft/core/LuaCoverage.java
@@ -46,24 +46,9 @@ class LuaCoverage {
}
void write(Writer out) throws IOException {
- Files.find(ROOT, Integer.MAX_VALUE, (path, attr) -> attr.isRegularFile()).forEach(path -> {
- var relative = ROOT.relativize(path);
- var full = relative.toString().replace('\\', '/');
- if (!full.endsWith(".lua")) return;
-
- var possiblePaths = coverage.remove("/" + full);
- if (possiblePaths == null) possiblePaths = coverage.remove(full);
- if (possiblePaths == null) {
- possiblePaths = Int2IntMaps.EMPTY_MAP;
- LOG.warn("{} has no coverage data", full);
- }
-
- try {
- writeCoverageFor(out, path, possiblePaths);
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- });
+ try (var files = Files.find(ROOT, Integer.MAX_VALUE, (path, attr) -> attr.isRegularFile())) {
+ files.forEach(path -> writeSingleFile(out, path));
+ }
for (var filename : coverage.keySet()) {
if (filename.startsWith("/test-rom/")) continue;
@@ -71,6 +56,25 @@ class LuaCoverage {
}
}
+ private void writeSingleFile(Writer out, Path path) {
+ var relative = ROOT.relativize(path);
+ var full = relative.toString().replace('\\', '/');
+ if (!full.endsWith(".lua")) return;
+
+ var possiblePaths = coverage.remove("/" + full);
+ if (possiblePaths == null) possiblePaths = coverage.remove(full);
+ if (possiblePaths == null) {
+ possiblePaths = Int2IntMaps.EMPTY_MAP;
+ LOG.warn("{} has no coverage data", full);
+ }
+
+ try {
+ writeCoverageFor(out, path, possiblePaths);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
private void writeCoverageFor(Writer out, Path fullName, Map visitedLines) throws IOException {
if (!Files.exists(fullName)) {
LOG.error("Cannot locate file {}", fullName);
diff --git a/projects/core/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java b/projects/core/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java
index 7a0e14cc5..4df1d4cab 100644
--- a/projects/core/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java
+++ b/projects/core/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java
@@ -101,7 +101,7 @@ public class ComputerBootstrap {
try {
context.ensureClosed(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
- throw new IllegalStateException("Runtime thread was interrupted", e);
+ Thread.currentThread().interrupt();
}
}
}
diff --git a/projects/core/src/test/java/dan200/computercraft/core/computer/computerthread/ComputerThreadRunner.java b/projects/core/src/test/java/dan200/computercraft/core/computer/computerthread/ComputerThreadRunner.java
index 81f22e92a..6a76d929a 100644
--- a/projects/core/src/test/java/dan200/computercraft/core/computer/computerthread/ComputerThreadRunner.java
+++ b/projects/core/src/test/java/dan200/computercraft/core/computer/computerthread/ComputerThreadRunner.java
@@ -65,7 +65,7 @@ public class ComputerThreadRunner implements AutoCloseable {
} finally {
errorLock.unlock();
}
- } while (worker.executed == 0 || thread.hasPendingWork());
+ } while (!worker.executed || thread.hasPendingWork());
}
@GuardedBy("errorLock")
@@ -87,7 +87,7 @@ public class ComputerThreadRunner implements AutoCloseable {
private final Task run;
private final ComputerScheduler.Executor executor;
private long[] totals = new long[16];
- volatile int executed = 0;
+ private volatile boolean executed = false;
private Worker(ComputerScheduler scheduler, Task run) {
this.run = run;
@@ -102,7 +102,7 @@ public class ComputerThreadRunner implements AutoCloseable {
public void work() {
try {
run.run(executor);
- executed++;
+ executed = true;
} catch (Throwable e) {
errorLock.lock();
try {
diff --git a/projects/core/src/test/resources/test-rom/spec/apis/colors_spec.lua b/projects/core/src/test/resources/test-rom/spec/apis/colors_spec.lua
index e6ff9f2d6..46c77dcf8 100644
--- a/projects/core/src/test/resources/test-rom/spec/apis/colors_spec.lua
+++ b/projects/core/src/test/resources/test-rom/spec/apis/colors_spec.lua
@@ -92,6 +92,11 @@ describe("The colors library", function()
it("floors colors", function()
expect(colors.toBlit(16385)):eq("e")
end)
+
+ it("errors on out-of-range colours", function()
+ expect.error(colors.toBlit, -120):eq("Colour out of range")
+ expect.error(colors.toBlit, 0x10000):eq("Colour out of range")
+ end)
end)
describe("colors.fromBlit", function()
diff --git a/projects/core/src/test/resources/test-rom/spec/apis/window_spec.lua b/projects/core/src/test/resources/test-rom/spec/apis/window_spec.lua
index cc53d8119..89e4e2993 100644
--- a/projects/core/src/test/resources/test-rom/spec/apis/window_spec.lua
+++ b/projects/core/src/test/resources/test-rom/spec/apis/window_spec.lua
@@ -58,7 +58,14 @@ describe("The window library", function()
w.setTextColour(colors.white)
expect.error(w.setTextColour, nil):eq("bad argument #1 (number expected, got nil)")
- expect.error(w.setTextColour, -5):eq("Invalid color (got -5)")
+ expect.error(w.setTextColour, -5):eq("Colour out of range")
+ end)
+
+ it("supports invalid combined colours", function()
+ local w = mk()
+ w.setTextColour(colours.combine(colours.red, colours.green))
+
+ expect(w.getTextColour()):eq(colours.red)
end)
end)
@@ -69,7 +76,7 @@ describe("The window library", function()
w.setPaletteColour(colors.white, 0x000000)
expect.error(w.setPaletteColour, nil):eq("bad argument #1 (number expected, got nil)")
- expect.error(w.setPaletteColour, -5):eq("Invalid color (got -5)")
+ expect.error(w.setPaletteColour, -5):eq("Colour out of range")
expect.error(w.setPaletteColour, colors.white):eq("bad argument #2 (number expected, got nil)")
expect.error(w.setPaletteColour, colors.white, 1, false):eq("bad argument #3 (number expected, got boolean)")
expect.error(w.setPaletteColour, colors.white, 1, nil, 1):eq("bad argument #3 (number expected, got nil)")
@@ -82,7 +89,7 @@ describe("The window library", function()
local w = mk()
w.getPaletteColour(colors.white)
expect.error(w.getPaletteColour, nil):eq("bad argument #1 (number expected, got nil)")
- expect.error(w.getPaletteColour, -5):eq("Invalid color (got -5)")
+ expect.error(w.getPaletteColour, -5):eq("Colour out of range")
end)
end)
@@ -92,7 +99,7 @@ describe("The window library", function()
w.setBackgroundColour(colors.white)
expect.error(w.setBackgroundColour, nil):eq("bad argument #1 (number expected, got nil)")
- expect.error(w.setBackgroundColour, -5):eq("Invalid color (got -5)")
+ expect.error(w.setBackgroundColour, -5):eq("Colour out of range")
end)
end)
diff --git a/projects/fabric/src/client/java/dan200/computercraft/client/ComputerCraftClient.java b/projects/fabric/src/client/java/dan200/computercraft/client/ComputerCraftClient.java
index e8a1bdaa2..45da56284 100644
--- a/projects/fabric/src/client/java/dan200/computercraft/client/ComputerCraftClient.java
+++ b/projects/fabric/src/client/java/dan200/computercraft/client/ComputerCraftClient.java
@@ -28,6 +28,7 @@ import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.MenuScreens;
import net.minecraft.client.renderer.RenderType;
+import net.minecraft.client.renderer.item.ItemProperties;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
@@ -47,7 +48,7 @@ public class ComputerCraftClient {
ClientRegistry.registerTurtleModellers(FabricComputerCraftAPIClient::registerTurtleUpgradeModeller);
ClientRegistry.registerItemColours(ColorProviderRegistry.ITEM::register);
ClientRegistry.registerMenuScreens(MenuScreens::register);
- ClientRegistry.registerMainThread();
+ ClientRegistry.registerMainThread(ItemProperties::register);
PreparableModelLoadingPlugin.register(CustomModelLoader::prepare, (state, context) -> {
ClientRegistry.registerExtraModels(context::addModels);
diff --git a/projects/fabric/src/generated/resources/assets/computercraft/lang/en_us.json b/projects/fabric/src/generated/resources/assets/computercraft/lang/en_us.json
index 4a34432d1..a53f4b41d 100644
--- a/projects/fabric/src/generated/resources/assets/computercraft/lang/en_us.json
+++ b/projects/fabric/src/generated/resources/assets/computercraft/lang/en_us.json
@@ -1,8 +1,14 @@
{
"argument.computercraft.argument_expected": "Argument expected",
+ "argument.computercraft.computer.distance": "Distance to entity",
+ "argument.computercraft.computer.family": "Computer family",
+ "argument.computercraft.computer.id": "Computer ID",
+ "argument.computercraft.computer.instance": "Unique instance ID",
+ "argument.computercraft.computer.label": "Computer label",
"argument.computercraft.computer.many_matching": "Multiple computers matching '%s' (instances %s)",
"argument.computercraft.computer.no_matching": "No computers matching '%s'",
"argument.computercraft.tracking_field.no_field": "Unknown field '%s'",
+ "argument.computercraft.unknown_computer_family": "Unknown computer family '%s'",
"block.computercraft.cable": "Networking Cable",
"block.computercraft.computer_advanced": "Advanced Computer",
"block.computercraft.computer_command": "Command Computer",
diff --git a/projects/fabric/src/main/java/dan200/computercraft/mixin/ChunkMapMixin.java b/projects/fabric/src/main/java/dan200/computercraft/mixin/ChunkMapMixin.java
new file mode 100644
index 000000000..e3d75b645
--- /dev/null
+++ b/projects/fabric/src/main/java/dan200/computercraft/mixin/ChunkMapMixin.java
@@ -0,0 +1,31 @@
+// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
+//
+// SPDX-License-Identifier: MPL-2.0
+
+package dan200.computercraft.mixin;
+
+import dan200.computercraft.shared.CommonHooks;
+import net.minecraft.server.level.ChunkHolder;
+import net.minecraft.server.level.ChunkMap;
+import net.minecraft.server.level.ServerLevel;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import javax.annotation.Nullable;
+
+@Mixin(ChunkMap.class)
+class ChunkMapMixin {
+ @Final
+ @Shadow
+ ServerLevel level;
+
+ @Inject(method = "updateChunkScheduling", at = @At("HEAD"))
+ @SuppressWarnings("UnusedMethod")
+ private void onUpdateChunkScheduling(long chunkPos, int newLevel, @Nullable ChunkHolder holder, int oldLevel, CallbackInfoReturnable callback) {
+ CommonHooks.onChunkTicketLevelChanged(level, chunkPos, oldLevel, newLevel);
+ }
+}
diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/ComputerCraft.java b/projects/fabric/src/main/java/dan200/computercraft/shared/ComputerCraft.java
index 5a99d3d1b..fd0951fc7 100644
--- a/projects/fabric/src/main/java/dan200/computercraft/shared/ComputerCraft.java
+++ b/projects/fabric/src/main/java/dan200/computercraft/shared/ComputerCraft.java
@@ -24,6 +24,7 @@ import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemBlockE
import dan200.computercraft.shared.platform.FabricConfigFile;
import dan200.computercraft.shared.platform.FabricMessageType;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
+import net.fabricmc.fabric.api.event.lifecycle.v1.ServerChunkEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
@@ -101,6 +102,7 @@ public class ComputerCraft {
ServerTickEvents.START_SERVER_TICK.register(CommonHooks::onServerTickStart);
ServerTickEvents.START_SERVER_TICK.register(s -> CommonHooks.onServerTickEnd());
+ ServerChunkEvents.CHUNK_UNLOAD.register((l, c) -> CommonHooks.onServerChunkUnload(c));
PlayerBlockBreakEvents.BEFORE.register(FabricCommonHooks::onBlockDestroy);
UseBlockCallback.EVENT.register(FabricCommonHooks::useOnBlock);
diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java b/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java
index e1848775b..f14753d37 100644
--- a/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java
+++ b/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java
@@ -13,6 +13,8 @@ import dan200.computercraft.shared.platform.FabricContainerTransfer;
import net.fabricmc.fabric.api.transfer.v1.item.ItemStorage;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
import net.fabricmc.fabric.api.transfer.v1.storage.SlottedStorage;
+import net.fabricmc.fabric.api.transfer.v1.storage.base.CombinedSlottedStorage;
+import net.fabricmc.fabric.api.transfer.v1.storage.base.CombinedStorage;
import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleSlotStorage;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
@@ -42,6 +44,34 @@ public final class InventoryMethods extends AbstractInventoryMethods storage) {
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (!(obj instanceof StorageWrapper other)) return false;
+
+ var otherStorage = other.storage;
+
+ /*
+ Equality for inventory storage isn't really defined, and most of the time falls back to reference
+ equality.
+ - Vanilla inventories are exposed via InventoryStorage - the creation of this is cached, so will be
+ the same object.
+ - Double chests are combined into a CombinedSlottedStorage. We check the parts are equal.
+ */
+ if (
+ storage instanceof CombinedSlottedStorage, ?> cs && storage.getClass() == otherStorage.getClass()
+ && cs.parts.equals(((CombinedStorage, ?>) otherStorage).parts)
+ ) {
+ return true;
+ }
+
+ return storage.equals(otherStorage);
+ }
+
+ @Override
+ public int hashCode() {
+ return storage instanceof CombinedSlottedStorage, ?> cs ? cs.parts.hashCode() : storage.hashCode();
+ }
}
@Override
diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FabricContainerTransfer.java b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FabricContainerTransfer.java
index a4b4303da..465eb3514 100644
--- a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FabricContainerTransfer.java
+++ b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FabricContainerTransfer.java
@@ -9,12 +9,11 @@ import net.fabricmc.fabric.api.transfer.v1.storage.SlottedStorage;
import net.fabricmc.fabric.api.transfer.v1.storage.Storage;
import net.fabricmc.fabric.api.transfer.v1.storage.StorageUtil;
import net.fabricmc.fabric.api.transfer.v1.storage.StorageView;
+import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
-import javax.annotation.Nullable;
import java.util.Iterator;
import java.util.NoSuchElementException;
-import java.util.function.Predicate;
@SuppressWarnings("UnstableApiUsage")
public class FabricContainerTransfer implements ContainerTransfer {
@@ -34,37 +33,31 @@ public class FabricContainerTransfer implements ContainerTransfer {
@Override
public int moveTo(ContainerTransfer destination, int maxAmount) {
- var predicate = new GatePredicate();
+ var hasItem = false;
- var moved = StorageUtil.move(storage, ((FabricContainerTransfer) destination).storage, predicate, maxAmount, null);
- if (moved > 0) return moved > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) moved;
+ var destStorage = ((FabricContainerTransfer) destination).storage;
+ for (var slot : storage.nonEmptyViews()) {
+ var resource = slot.getResource();
- // Nasty hack here to check if move() actually found an item in the original inventory. Saves having to
- // iterate over the source twice.
- return predicate.hasItem() ? NO_SPACE : NO_ITEMS;
- }
+ try (var transaction = Transaction.openOuter()) {
+ // Check how much can be extracted and inserted.
+ var maxExtracted = StorageUtil.simulateExtract(slot, resource, maxAmount, transaction);
+ if (maxExtracted == 0) continue;
- /**
- * A predicate which accepts the first value it sees, and then only those matching that value.
- *
- * @param The type of the object to accept.
- */
- private static final class GatePredicate implements Predicate {
- private @Nullable T instance = null;
+ hasItem = true;
- @Override
- public boolean test(T o) {
- if (instance == null) {
- instance = o;
- return true;
+ var accepted = destStorage.insert(resource, maxExtracted, transaction);
+ if (accepted == 0) continue;
+
+ // Extract or rollback.
+ if (slot.extract(resource, accepted, transaction) == accepted) {
+ transaction.commit();
+ return (int) accepted;
+ }
}
-
- return instance.equals(o);
}
- boolean hasItem() {
- return instance != null;
- }
+ return hasItem ? NO_SPACE : NO_ITEMS;
}
private static final class SlottedImpl extends FabricContainerTransfer implements ContainerTransfer.Slotted {
diff --git a/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientRegistry.java b/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientRegistry.java
index 69c32e133..212075c51 100644
--- a/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientRegistry.java
+++ b/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientRegistry.java
@@ -9,6 +9,7 @@ import dan200.computercraft.api.client.turtle.RegisterTurtleModellersEvent;
import dan200.computercraft.client.model.turtle.TurtleModelLoader;
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.item.ItemProperties;
import net.minecraft.resources.ResourceLocation;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.SubscribeEvent;
@@ -86,6 +87,6 @@ public final class ForgeClientRegistry {
@SubscribeEvent
public static void setupClient(FMLClientSetupEvent event) {
ClientRegistry.register();
- event.enqueueWork(ClientRegistry::registerMainThread);
+ event.enqueueWork(() -> ClientRegistry.registerMainThread(ItemProperties::register));
}
}
diff --git a/projects/forge/src/generated/resources/assets/computercraft/lang/en_us.json b/projects/forge/src/generated/resources/assets/computercraft/lang/en_us.json
index 4a34432d1..a53f4b41d 100644
--- a/projects/forge/src/generated/resources/assets/computercraft/lang/en_us.json
+++ b/projects/forge/src/generated/resources/assets/computercraft/lang/en_us.json
@@ -1,8 +1,14 @@
{
"argument.computercraft.argument_expected": "Argument expected",
+ "argument.computercraft.computer.distance": "Distance to entity",
+ "argument.computercraft.computer.family": "Computer family",
+ "argument.computercraft.computer.id": "Computer ID",
+ "argument.computercraft.computer.instance": "Unique instance ID",
+ "argument.computercraft.computer.label": "Computer label",
"argument.computercraft.computer.many_matching": "Multiple computers matching '%s' (instances %s)",
"argument.computercraft.computer.no_matching": "No computers matching '%s'",
"argument.computercraft.tracking_field.no_field": "Unknown field '%s'",
+ "argument.computercraft.unknown_computer_family": "Unknown computer family '%s'",
"block.computercraft.cable": "Networking Cable",
"block.computercraft.computer_advanced": "Advanced Computer",
"block.computercraft.computer_command": "Command Computer",
diff --git a/projects/forge/src/main/java/dan200/computercraft/shared/ForgeCommonHooks.java b/projects/forge/src/main/java/dan200/computercraft/shared/ForgeCommonHooks.java
index 33ff2330a..4aed5aaac 100644
--- a/projects/forge/src/main/java/dan200/computercraft/shared/ForgeCommonHooks.java
+++ b/projects/forge/src/main/java/dan200/computercraft/shared/ForgeCommonHooks.java
@@ -8,12 +8,16 @@ import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.shared.command.CommandComputerCraft;
import dan200.computercraft.shared.network.client.UpgradesLoadedMessage;
import dan200.computercraft.shared.network.server.ServerNetworking;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.chunk.LevelChunk;
import net.neoforged.bus.api.EventPriority;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.Mod;
import net.neoforged.neoforge.event.*;
import net.neoforged.neoforge.event.entity.EntityJoinLevelEvent;
import net.neoforged.neoforge.event.entity.living.LivingDropsEvent;
+import net.neoforged.neoforge.event.level.ChunkEvent;
+import net.neoforged.neoforge.event.level.ChunkTicketLevelUpdatedEvent;
import net.neoforged.neoforge.event.level.ChunkWatchEvent;
import net.neoforged.neoforge.event.server.ServerStartedEvent;
import net.neoforged.neoforge.event.server.ServerStartingEvent;
@@ -52,11 +56,23 @@ public class ForgeCommonHooks {
CommandComputerCraft.register(event.getDispatcher());
}
+ @SubscribeEvent
+ public static void onChunkUnload(ChunkEvent.Unload event) {
+ if (event.getLevel() instanceof ServerLevel && event.getChunk() instanceof LevelChunk chunk) {
+ CommonHooks.onServerChunkUnload(chunk);
+ }
+ }
+
@SubscribeEvent
public static void onChunkWatch(ChunkWatchEvent.Sent event) {
CommonHooks.onChunkWatch(event.getChunk(), event.getPlayer());
}
+ @SubscribeEvent
+ public static void onChunkTicketLevelChanged(ChunkTicketLevelUpdatedEvent event) {
+ CommonHooks.onChunkTicketLevelChanged(event.getLevel(), event.getChunkPos(), event.getOldTicketLevel(), event.getNewTicketLevel());
+ }
+
@SubscribeEvent
public static void onAddReloadListeners(AddReloadListenerEvent event) {
CommonHooks.onDatapackReload((id, listener) -> event.addListener(listener));
diff --git a/projects/forge/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java b/projects/forge/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java
index 8cb0694b6..ff8bd8111 100644
--- a/projects/forge/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java
+++ b/projects/forge/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java
@@ -312,6 +312,11 @@ public class PlatformHelperImpl implements PlatformHelper {
return event.getUseItem() == Event.Result.DENY ? InteractionResult.PASS : stack.useOn(context);
}
+ @Override
+ public boolean canClickRunClientCommand() {
+ return false;
+ }
+
private record RegistrationHelperImpl(DeferredRegister registry) implements RegistrationHelper {
@Override
public RegistryEntry register(String name, Supplier create) {
diff --git a/projects/lints/src/main/kotlin/cc/tweaked/linter/SideChecker.kt b/projects/lints/src/main/kotlin/cc/tweaked/linter/SideChecker.kt
index e01748111..958051ef3 100644
--- a/projects/lints/src/main/kotlin/cc/tweaked/linter/SideChecker.kt
+++ b/projects/lints/src/main/kotlin/cc/tweaked/linter/SideChecker.kt
@@ -105,7 +105,6 @@ internal class SideProvider {
companion object {
private val notClientPackages = listOf(
// Ugly! But we do what we must.
- "net.fabricmc.fabric.api.client.itemgroup",
"dan200.computercraft.shared.network.client",
)
diff --git a/projects/web/build.gradle.kts b/projects/web/build.gradle.kts
index c1df434dd..25f0838b6 100644
--- a/projects/web/build.gradle.kts
+++ b/projects/web/build.gradle.kts
@@ -138,11 +138,6 @@ val docWebsite by tasks.registering(Copy::class) {
from(htmlTransform)
- // Pick up assets from the /docs folder
- from(rootProject.file("doc")) {
- include("logo.png")
- include("images/**")
- }
// index.js is provided by illuaminate, but rollup outputs some other chunks
from(rollup) { exclude("index.js") }
// Grab illuaminate's assets. HTML files are provided by jsxDocs
diff --git a/projects/web/src/builder/java/cc/tweaked/web/builder/Builder.java b/projects/web/src/builder/java/cc/tweaked/web/builder/Builder.java
index 37687ffc3..6bcc97038 100644
--- a/projects/web/src/builder/java/cc/tweaked/web/builder/Builder.java
+++ b/projects/web/src/builder/java/cc/tweaked/web/builder/Builder.java
@@ -4,6 +4,7 @@
package cc.tweaked.web.builder;
+import org.teavm.backend.javascript.JSModuleType;
import org.teavm.common.JsonUtil;
import org.teavm.tooling.ConsoleTeaVMToolLog;
import org.teavm.tooling.TeaVMProblemRenderer;
@@ -71,6 +72,7 @@ public class Builder {
// Then finally start the compiler!
var tool = new TeaVMTool();
tool.setTargetType(TeaVMTargetType.JAVASCRIPT);
+ tool.setJsModuleType(JSModuleType.ES2015);
tool.setTargetDirectory(output.toFile());
tool.setClassLoader(remapper);
tool.setMainClass("cc.tweaked.web.Main");
diff --git a/projects/web/src/main/java/cc/tweaked/web/EmulatedComputer.java b/projects/web/src/main/java/cc/tweaked/web/EmulatedComputer.java
index 9565eb481..c39467d74 100644
--- a/projects/web/src/main/java/cc/tweaked/web/EmulatedComputer.java
+++ b/projects/web/src/main/java/cc/tweaked/web/EmulatedComputer.java
@@ -30,6 +30,7 @@ import org.teavm.jso.typedarrays.ArrayBuffer;
import javax.annotation.Nullable;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
+import java.util.Objects;
/**
* Manages the core lifecycle of an emulated {@link Computer}.
@@ -47,6 +48,9 @@ class EmulatedComputer implements ComputerEnvironment, ComputerHandle {
private boolean disposed = false;
private final MemoryMount mount = new MemoryMount();
+ private @Nullable String oldLabel;
+ private boolean oldOn;
+
EmulatedComputer(ComputerContext context, ComputerDisplay computerAccess) {
this.computerAccess = computerAccess;
this.computer = new Computer(context, this, terminal, 0);
@@ -68,8 +72,10 @@ class EmulatedComputer implements ComputerEnvironment, ComputerHandle {
LOG.error("Error when ticking computer", e);
}
- if (computer.pollAndResetChanged()) {
- computerAccess.setState(computer.getLabel(), computer.isOn());
+ var newLabel = computer.getLabel();
+ var newOn = computer.isOn();
+ if (!Objects.equals(oldLabel, newLabel) || oldOn != newOn) {
+ computerAccess.setState(oldLabel = newLabel, oldOn = newOn);
}
for (var side : SIDES) {
diff --git a/projects/web/src/main/java/cc/tweaked/web/js/JavascriptConv.java b/projects/web/src/main/java/cc/tweaked/web/js/JavascriptConv.java
index 18f2732e0..713fc0934 100644
--- a/projects/web/src/main/java/cc/tweaked/web/js/JavascriptConv.java
+++ b/projects/web/src/main/java/cc/tweaked/web/js/JavascriptConv.java
@@ -78,11 +78,11 @@ public class JavascriptConv {
* @return The wrapped array.
*/
public static byte[] asByteArray(ArrayBuffer view) {
- return asByteArray(Int8Array.create(view));
+ return asByteArray(new Int8Array(view));
}
public static Int8Array toArray(ByteBuffer buffer) {
- var array = Int8Array.create(buffer.remaining());
+ var array = new Int8Array(buffer.remaining());
for (var i = 0; i < array.getLength(); i++) array.set(i, buffer.get(i));
return array;
}
diff --git a/projects/web/src/main/java/cc/tweaked/web/peripheral/SpeakerPeripheral.java b/projects/web/src/main/java/cc/tweaked/web/peripheral/SpeakerPeripheral.java
index 5faff07ee..d4f57ad3b 100644
--- a/projects/web/src/main/java/cc/tweaked/web/peripheral/SpeakerPeripheral.java
+++ b/projects/web/src/main/java/cc/tweaked/web/peripheral/SpeakerPeripheral.java
@@ -53,11 +53,13 @@ public class SpeakerPeripheral implements TickablePeripheral {
}
@LuaFunction
+ @SuppressWarnings("DoNotCallSuggester")
public final boolean playNote(String instrumentA, Optional volumeA, Optional pitchA) throws LuaException {
throw new LuaException("Cannot play notes outside of Minecraft");
}
@LuaFunction
+ @SuppressWarnings("DoNotCallSuggester")
public final boolean playSound(String name, Optional volumeA, Optional pitchA) throws LuaException {
throw new LuaException("Cannot play sounds outside of Minecraft");
}
@@ -70,7 +72,7 @@ public class SpeakerPeripheral implements TickablePeripheral {
if (length <= 0) throw new LuaException("Cannot play empty audio");
if (length > 128 * 1024) throw new LuaException("Audio data is too large");
- if (audioContext == null) audioContext = AudioContext.create();
+ if (audioContext == null) audioContext = new AudioContext();
if (state == null || !state.isPlaying()) state = new AudioState(audioContext);
return state.pushBuffer(audio, length, volume);
diff --git a/projects/web/src/main/java/dan200/computercraft/core/apis/http/request/THttpRequest.java b/projects/web/src/main/java/dan200/computercraft/core/apis/http/request/THttpRequest.java
index d8318ae09..13f9d48ee 100644
--- a/projects/web/src/main/java/dan200/computercraft/core/apis/http/request/THttpRequest.java
+++ b/projects/web/src/main/java/dan200/computercraft/core/apis/http/request/THttpRequest.java
@@ -92,7 +92,7 @@ public class THttpRequest extends Resource {
if (isClosed()) return;
try {
- var request = XMLHttpRequest.create();
+ var request = new XMLHttpRequest();
request.setOnReadyStateChange(() -> onResponseStateChange(request));
request.setResponseType("arraybuffer");
var address = uri.toASCIIString();
diff --git a/projects/web/src/main/java/dan200/computercraft/core/apis/http/websocket/TWebsocket.java b/projects/web/src/main/java/dan200/computercraft/core/apis/http/websocket/TWebsocket.java
index e90e5a4f2..671eeb506 100644
--- a/projects/web/src/main/java/dan200/computercraft/core/apis/http/websocket/TWebsocket.java
+++ b/projects/web/src/main/java/dan200/computercraft/core/apis/http/websocket/TWebsocket.java
@@ -40,7 +40,7 @@ public class TWebsocket extends Resource implements WebsocketClient
public void connect() {
if (isClosed()) return;
- var client = this.websocket = WebSocket.create(uri.toASCIIString());
+ var client = this.websocket = new WebSocket(uri.toASCIIString());
client.setBinaryType("arraybuffer");
client.onOpen(e -> success(Action.ALLOW.toPartial().toOptions()));
client.onError(e -> {
@@ -50,7 +50,7 @@ public class TWebsocket extends Resource implements WebsocketClient
client.onMessage(e -> {
if (isClosed()) return;
if (JavascriptConv.isArrayBuffer(e.getData())) {
- var array = Int8Array.create(e.getDataAsArray());
+ var array = new Int8Array(e.getDataAsArray());
var contents = new byte[array.getLength()];
for (var i = 0; i < contents.length; i++) contents[i] = array.get(i);
environment.queueEvent("websocket_message", address, contents, true);