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
+ * 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/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