mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-11-03 15:13:07 +00:00
Compare commits
11 Commits
v1.20.1-1.
...
v1.20.1-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3493159a05 | ||
|
|
eead67e314 | ||
|
|
3b8813cf8f | ||
|
|
a9191a4d4e | ||
|
|
451a2593ce | ||
|
|
d38b1da974 | ||
|
|
6e374579a4 | ||
|
|
4daa2a2b6a | ||
|
|
84b6edab82 | ||
|
|
31aaf46d09 | ||
|
|
2d11b51c62 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@
|
||||
/projects/*/logs
|
||||
/projects/fabric/fabricloader.log
|
||||
/projects/*/build
|
||||
/projects/*/src/test/generated_tests/
|
||||
/buildSrc/build
|
||||
/out
|
||||
/buildSrc/out
|
||||
|
||||
@@ -10,7 +10,7 @@ kotlin.jvm.target.validation.mode=error
|
||||
|
||||
# Mod properties
|
||||
isUnstable=false
|
||||
modVersion=1.109.6
|
||||
modVersion=1.109.7
|
||||
|
||||
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
|
||||
mcVersion=1.20.1
|
||||
|
||||
@@ -51,6 +51,7 @@ sodium = "mc1.20-0.4.10"
|
||||
hamcrest = "2.2"
|
||||
jqwik = "1.8.2"
|
||||
junit = "5.10.1"
|
||||
jmh = "1.37"
|
||||
|
||||
# Build tools
|
||||
cctJavadoc = "1.8.2"
|
||||
@@ -127,6 +128,8 @@ junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.re
|
||||
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }
|
||||
junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" }
|
||||
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
|
||||
jmh = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" }
|
||||
jmh-processor = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" }
|
||||
|
||||
# LWJGL
|
||||
lwjgl-bom = { module = "org.lwjgl:lwjgl-bom", version.ref = "lwjgl" }
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package dan200.computercraft.api.network.wired;
|
||||
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@@ -22,6 +23,7 @@ import java.util.Map;
|
||||
*
|
||||
* @see WiredNode#getNetwork()
|
||||
*/
|
||||
@ApiStatus.NonExtendable
|
||||
public interface WiredNetwork {
|
||||
/**
|
||||
* Create a connection between two nodes.
|
||||
@@ -35,7 +37,9 @@ public interface WiredNetwork {
|
||||
* @throws IllegalArgumentException If {@code left} and {@code right} are equal.
|
||||
* @see WiredNode#connectTo(WiredNode)
|
||||
* @see WiredNetwork#connect(WiredNode, WiredNode)
|
||||
* @deprecated Use {@link WiredNode#connectTo(WiredNode)}
|
||||
*/
|
||||
@Deprecated
|
||||
boolean connect(WiredNode left, WiredNode right);
|
||||
|
||||
/**
|
||||
@@ -50,7 +54,9 @@ public interface WiredNetwork {
|
||||
* @throws IllegalArgumentException If {@code left} and {@code right} are equal.
|
||||
* @see WiredNode#disconnectFrom(WiredNode)
|
||||
* @see WiredNetwork#connect(WiredNode, WiredNode)
|
||||
* @deprecated Use {@link WiredNode#disconnectFrom(WiredNode)}
|
||||
*/
|
||||
@Deprecated
|
||||
boolean disconnect(WiredNode left, WiredNode right);
|
||||
|
||||
/**
|
||||
@@ -64,7 +70,9 @@ public interface WiredNetwork {
|
||||
* only element.
|
||||
* @throws IllegalArgumentException If the node is not in the network.
|
||||
* @see WiredNode#remove()
|
||||
* @deprecated Use {@link WiredNode#remove()}
|
||||
*/
|
||||
@Deprecated
|
||||
boolean remove(WiredNode node);
|
||||
|
||||
/**
|
||||
@@ -77,6 +85,8 @@ public interface WiredNetwork {
|
||||
* @param peripherals The new peripherals for this node.
|
||||
* @throws IllegalArgumentException If the node is not in the network.
|
||||
* @see WiredNode#updatePeripherals(Map)
|
||||
* @deprecated Use {@link WiredNode#updatePeripherals(Map)}
|
||||
*/
|
||||
@Deprecated
|
||||
void updatePeripherals(WiredNode node, Map<String, IPeripheral> peripherals);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package dan200.computercraft.api.network.wired;
|
||||
|
||||
import dan200.computercraft.api.network.PacketNetwork;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@@ -22,6 +23,7 @@ import java.util.Map;
|
||||
* Wired nodes also provide several convenience methods for interacting with a wired network. These should only ever
|
||||
* be used on the main server thread.
|
||||
*/
|
||||
@ApiStatus.NonExtendable
|
||||
public interface WiredNode extends PacketNetwork {
|
||||
/**
|
||||
* The associated element for this network node.
|
||||
@@ -37,7 +39,9 @@ public interface WiredNode extends PacketNetwork {
|
||||
* This should only be used on the server thread.
|
||||
*
|
||||
* @return This node's network.
|
||||
* @deprecated Use the connect/disconnect/remove methods on {@link WiredNode}.
|
||||
*/
|
||||
@Deprecated
|
||||
WiredNetwork getNetwork();
|
||||
|
||||
/**
|
||||
@@ -47,12 +51,9 @@ public interface WiredNode extends PacketNetwork {
|
||||
*
|
||||
* @param node The other node to connect to.
|
||||
* @return {@code true} if a connection was created or {@code false} if the connection already exists.
|
||||
* @see WiredNetwork#connect(WiredNode, WiredNode)
|
||||
* @see WiredNode#disconnectFrom(WiredNode)
|
||||
*/
|
||||
default boolean connectTo(WiredNode node) {
|
||||
return getNetwork().connect(this, node);
|
||||
}
|
||||
boolean connectTo(WiredNode node);
|
||||
|
||||
/**
|
||||
* Destroy a connection between this node and another.
|
||||
@@ -61,13 +62,9 @@ public interface WiredNode extends PacketNetwork {
|
||||
*
|
||||
* @param node The other node to disconnect from.
|
||||
* @return {@code true} if a connection was destroyed or {@code false} if no connection exists.
|
||||
* @throws IllegalArgumentException If {@code node} is not on the same network.
|
||||
* @see WiredNetwork#disconnect(WiredNode, WiredNode)
|
||||
* @see WiredNode#connectTo(WiredNode)
|
||||
*/
|
||||
default boolean disconnectFrom(WiredNode node) {
|
||||
return getNetwork().disconnect(this, node);
|
||||
}
|
||||
boolean disconnectFrom(WiredNode node);
|
||||
|
||||
/**
|
||||
* Sever all connections this node has, removing it from this network.
|
||||
@@ -78,11 +75,8 @@ public interface WiredNode extends PacketNetwork {
|
||||
* @return Whether this node was removed from the network. One cannot remove a node from a network where it is the
|
||||
* only element.
|
||||
* @throws IllegalArgumentException If the node is not in the network.
|
||||
* @see WiredNetwork#remove(WiredNode)
|
||||
*/
|
||||
default boolean remove() {
|
||||
return getNetwork().remove(this);
|
||||
}
|
||||
boolean remove();
|
||||
|
||||
/**
|
||||
* Mark this node's peripherals as having changed.
|
||||
@@ -91,9 +85,6 @@ public interface WiredNode extends PacketNetwork {
|
||||
* that your network element owns.
|
||||
*
|
||||
* @param peripherals The new peripherals for this node.
|
||||
* @see WiredNetwork#updatePeripherals(WiredNode, Map)
|
||||
*/
|
||||
default void updatePeripherals(Map<String, IPeripheral> peripherals) {
|
||||
getNetwork().updatePeripherals(this, peripherals);
|
||||
}
|
||||
void updatePeripherals(Map<String, IPeripheral> peripherals);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ dependencies {
|
||||
testImplementation(libs.bundles.test)
|
||||
testRuntimeOnly(libs.bundles.testRuntime)
|
||||
|
||||
testImplementation(libs.jmh)
|
||||
testAnnotationProcessor(libs.jmh.processor)
|
||||
|
||||
testModCompileOnly(libs.mixin)
|
||||
testModImplementation(testFixtures(project(":core")))
|
||||
testModImplementation(testFixtures(project(":common")))
|
||||
|
||||
@@ -58,9 +58,9 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
|
||||
@Override
|
||||
public void render(MonitorBlockEntity monitor, float partialTicks, PoseStack transform, MultiBufferSource bufferSource, int lightmapCoord, int overlayLight) {
|
||||
// Render from the origin monitor
|
||||
var originTerminal = monitor.getClientMonitor();
|
||||
|
||||
var originTerminal = monitor.getOriginClientMonitor();
|
||||
if (originTerminal == null) return;
|
||||
|
||||
var origin = originTerminal.getOrigin();
|
||||
var renderState = originTerminal.getRenderState(MonitorRenderState::new);
|
||||
var monitorPos = monitor.getBlockPos();
|
||||
|
||||
@@ -4,45 +4,66 @@
|
||||
|
||||
package dan200.computercraft.impl.network.wired;
|
||||
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Verifies certain elements of a network are "well formed".
|
||||
* Verifies certain elements of a network are well-formed.
|
||||
* <p>
|
||||
* This adds substantial overhead to network modification, and so should only be enabled
|
||||
* in a development environment.
|
||||
* This adds substantial overhead to network modification, and so is only enabled when assertions are enabled.
|
||||
*/
|
||||
public final class InvariantChecker {
|
||||
final class InvariantChecker {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(InvariantChecker.class);
|
||||
private static final boolean ENABLED = false;
|
||||
|
||||
private InvariantChecker() {
|
||||
}
|
||||
|
||||
public static void checkNode(WiredNodeImpl node) {
|
||||
if (!ENABLED) return;
|
||||
static void checkNode(WiredNodeImpl node) {
|
||||
assert checkNodeImpl(node) : "Node invariants failed. See logs.";
|
||||
}
|
||||
|
||||
var network = node.network;
|
||||
if (network == null) {
|
||||
LOG.error("Node's network is null", new Exception());
|
||||
return;
|
||||
private static boolean checkNodeImpl(WiredNodeImpl node) {
|
||||
var okay = true;
|
||||
|
||||
if (node.currentSet != null) {
|
||||
okay = false;
|
||||
LOG.error("{}: currentSet was not cleared.", node);
|
||||
}
|
||||
|
||||
if (network.nodes == null || !network.nodes.contains(node)) {
|
||||
LOG.error("Node's network does not contain node", new Exception());
|
||||
var network = makeNullable(node.network);
|
||||
if (network == null) {
|
||||
okay = false;
|
||||
LOG.error("{}: Node's network is null.", node);
|
||||
} else if (makeNullable(network.nodes) == null || !network.nodes.contains(node)) {
|
||||
okay = false;
|
||||
LOG.error("{}: Node's network does not contain node.", node);
|
||||
}
|
||||
|
||||
for (var neighbour : node.neighbours) {
|
||||
if (!neighbour.neighbours.contains(node)) {
|
||||
LOG.error("Neighbour is missing node", new Exception());
|
||||
okay = false;
|
||||
LOG.error("{}: Neighbour {}'s neighbour set does not contain origianl node.", node, neighbour);
|
||||
}
|
||||
}
|
||||
|
||||
return okay;
|
||||
}
|
||||
|
||||
public static void checkNetwork(WiredNetworkImpl network) {
|
||||
if (!ENABLED) return;
|
||||
static void checkNetwork(WiredNetworkImpl network) {
|
||||
assert checkNetworkImpl(network) : "Network invariants failed. See logs.";
|
||||
}
|
||||
|
||||
for (var node : network.nodes) checkNode(node);
|
||||
private static boolean checkNetworkImpl(WiredNetworkImpl network) {
|
||||
var okay = true;
|
||||
for (var node : network.nodes) okay &= checkNodeImpl(node);
|
||||
return okay;
|
||||
}
|
||||
|
||||
@Contract("")
|
||||
private static <T> @Nullable T makeNullable(T object) {
|
||||
return object;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.impl.network.wired;
|
||||
|
||||
import dan200.computercraft.api.network.wired.WiredNode;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A disjoint-set/union-find of {@link WiredNodeImpl}s.
|
||||
* <p>
|
||||
* Rather than actually maintaining a list of included nodes, wired nodes store {@linkplain WiredNodeImpl#currentSet the
|
||||
* set they're part of}. This means that we can only have one disjoint-set at once, but that is not a problem in
|
||||
* practice.
|
||||
*
|
||||
* @see WiredNodeImpl#currentSet
|
||||
* @see WiredNetworkImpl#remove(WiredNode)
|
||||
* @see <a href="https://en.wikipedia.org/wiki/Disjoint-set_data_structure">Disjoint-set data structure</a>
|
||||
*/
|
||||
class NodeSet {
|
||||
private NodeSet parent = this;
|
||||
private int size = 1;
|
||||
private @Nullable WiredNetworkImpl network;
|
||||
|
||||
private boolean isRoot() {
|
||||
return parent == this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve this union, finding the root {@link NodeSet}.
|
||||
*
|
||||
* @return The root union.
|
||||
*/
|
||||
NodeSet find() {
|
||||
var self = this;
|
||||
while (!self.isRoot()) self = self.parent = self.parent.parent;
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of this node set.
|
||||
*
|
||||
* @return The size of the set.
|
||||
*/
|
||||
int size() {
|
||||
return find().size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a node to this {@link NodeSet}.
|
||||
*
|
||||
* @param node The node to add to the set.
|
||||
*/
|
||||
void addNode(WiredNodeImpl node) {
|
||||
if (!isRoot()) throw new IllegalStateException("Cannot grow a non-root set.");
|
||||
if (node.currentSet != null) throw new IllegalArgumentException("Node is already in a set.");
|
||||
|
||||
node.currentSet = this;
|
||||
size++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two nodes sets together.
|
||||
*
|
||||
* @param left The first union.
|
||||
* @param right The second union.
|
||||
* @return The union which was subsumed.
|
||||
*/
|
||||
public static NodeSet merge(NodeSet left, NodeSet right) {
|
||||
if (!left.isRoot() || !right.isRoot()) throw new IllegalArgumentException("Cannot union a non-root set.");
|
||||
if (left == right) throw new IllegalArgumentException("Cannot merge a node into itself.");
|
||||
|
||||
return left.size >= right.size ? mergeInto(left, right) : mergeInto(right, left);
|
||||
}
|
||||
|
||||
private static NodeSet mergeInto(NodeSet root, NodeSet child) {
|
||||
assert root.size > child.size;
|
||||
child.parent = root;
|
||||
root.size += child.size;
|
||||
return child;
|
||||
}
|
||||
|
||||
void setNetwork(WiredNetworkImpl network) {
|
||||
if (!isRoot()) throw new IllegalStateException("Set is not the root.");
|
||||
if (this.network != null) throw new IllegalStateException("Set already has a network.");
|
||||
this.network = network;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the associated network.
|
||||
*
|
||||
* @return The associated network.
|
||||
*/
|
||||
WiredNetworkImpl network() {
|
||||
return Objects.requireNonNull(find().network);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import dan200.computercraft.api.network.Packet;
|
||||
import dan200.computercraft.api.network.wired.WiredNetwork;
|
||||
import dan200.computercraft.api.network.wired.WiredNode;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.core.util.Nullability;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
@@ -187,10 +188,76 @@ final class WiredNetworkImpl implements WiredNetwork {
|
||||
return true;
|
||||
}
|
||||
|
||||
var reachable = reachableNodes(neighbours.iterator().next());
|
||||
assert neighbours.size() >= 2 : "Must have more than one neighbour.";
|
||||
|
||||
/*
|
||||
Otherwise we need to find all sets of connected nodes within the graph, and split them off into their own
|
||||
networks.
|
||||
|
||||
With our current graph representation[^1], this requires a traversal of the graph, taking O(|V| + |E))
|
||||
time, which can get quite expensive for large graphs. We try to avoid this traversal where possible, by
|
||||
optimising for the case where the graph remains fully connected after removing this node, for instance,
|
||||
removing "A" here:
|
||||
|
||||
A---B B
|
||||
| | => |
|
||||
C---D C---D
|
||||
|
||||
We observe that these sorts of loops tend to be local, and so try to identify them as quickly as possible.
|
||||
To do this, we do a standard breadth-first traversal of the graph starting at the neighbours of the
|
||||
removed node, building sets of connected nodes.
|
||||
|
||||
If, at any point, all nodes visited so far are connected to each other, then we know all remaining nodes
|
||||
will also be connected. This allows us to abort our traversal of the graph, and just remove the node (much
|
||||
like we do in the single neighbour case above).
|
||||
|
||||
Otherwise, we then just create a new network for each disjoint set of connected nodes.
|
||||
|
||||
{^1]:
|
||||
There are efficient (near-logarithmic) algorithms for this (e.g. https://arxiv.org/pdf/1609.05867.pdf),
|
||||
but they are significantly more complex to implement.
|
||||
*/
|
||||
|
||||
// Create a new set of nodes for each neighbour, and add them to our queue of nodes to visit.
|
||||
List<WiredNodeImpl> queue = new ArrayList<>();
|
||||
Set<NodeSet> nodeSets = new HashSet<>(neighbours.size());
|
||||
for (var neighbour : neighbours) {
|
||||
nodeSets.add(neighbour.currentSet = new NodeSet());
|
||||
queue.add(neighbour);
|
||||
}
|
||||
|
||||
// Perform a breadth-first search of the graph, starting from the neighbours.
|
||||
graphSearch:
|
||||
for (var i = 0; i < queue.size(); i++) {
|
||||
var enqueuedNode = queue.get(i);
|
||||
for (var neighbour : enqueuedNode.neighbours) {
|
||||
var nodeSet = Nullability.assertNonNull(enqueuedNode.currentSet).find();
|
||||
|
||||
// The neighbour has no set and so has not been visited yet. Add it to the current set and enqueue
|
||||
// it to be visited.
|
||||
if (neighbour.currentSet == null) {
|
||||
nodeSet.addNode(neighbour);
|
||||
queue.add(neighbour);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, take the union of the two nodes' sets if needed. If we've only got a single node set
|
||||
// left, then we know the whole graph is network is connected (even if not all nodes have been
|
||||
// visited) and so can abort early.
|
||||
var neighbourSet = neighbour.currentSet.find();
|
||||
if (nodeSet != neighbourSet) {
|
||||
var removed = nodeSets.remove(NodeSet.merge(nodeSet, neighbourSet));
|
||||
assert removed : "Merged set should have been ";
|
||||
if (nodeSets.size() == 1) break graphSearch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a single subset, then all nodes are reachable - just clear the set and exit.
|
||||
if (nodeSets.size() == 1) {
|
||||
assert nodeSets.iterator().next().size() == queue.size();
|
||||
for (var neighbour : queue) neighbour.currentSet = null;
|
||||
|
||||
// If all nodes are reachable then exit.
|
||||
if (reachable.size() == nodes.size()) {
|
||||
// Broadcast our simple peripheral changes
|
||||
removeSingleNode(wired, wiredNetwork);
|
||||
InvariantChecker.checkNode(wired);
|
||||
@@ -198,43 +265,46 @@ final class WiredNetworkImpl implements WiredNetwork {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A split may cause 2..neighbours.size() separate networks, so we
|
||||
// iterate through our neighbour list, generating child networks.
|
||||
neighbours.removeAll(reachable);
|
||||
var maximals = new ArrayList<WiredNetworkImpl>(neighbours.size() + 1);
|
||||
maximals.add(wiredNetwork);
|
||||
maximals.add(new WiredNetworkImpl(reachable));
|
||||
assert queue.size() == nodes.size() : "Expected queue to contain all nodes.";
|
||||
|
||||
while (!neighbours.isEmpty()) {
|
||||
reachable = reachableNodes(neighbours.iterator().next());
|
||||
neighbours.removeAll(reachable);
|
||||
maximals.add(new WiredNetworkImpl(reachable));
|
||||
// Otherwise we need to create our new networks.
|
||||
var networks = new ArrayList<WiredNetworkImpl>(1 + nodeSets.size());
|
||||
// Add the network we've created for the removed node.
|
||||
networks.add(wiredNetwork);
|
||||
// And then create a new network for each disjoint subset.
|
||||
for (var set : nodeSets) {
|
||||
var network = new WiredNetworkImpl(new HashSet<>(set.size()));
|
||||
set.setNetwork(network);
|
||||
networks.add(network);
|
||||
}
|
||||
|
||||
for (var network : maximals) network.lock.writeLock().lock();
|
||||
for (var network : networks) network.lock.writeLock().lock();
|
||||
|
||||
try {
|
||||
// We special case the original node: detaching all peripherals when needed.
|
||||
wired.network = wiredNetwork;
|
||||
wired.peripherals = Map.of();
|
||||
wired.neighbours.clear();
|
||||
|
||||
// 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<WiredNodeImpl> reachableNodes(WiredNodeImpl start) {
|
||||
Queue<WiredNodeImpl> enqueued = new ArrayDeque<>();
|
||||
var reachable = new HashSet<WiredNodeImpl>();
|
||||
|
||||
reachable.add(start);
|
||||
enqueued.add(start);
|
||||
|
||||
WiredNodeImpl node;
|
||||
while ((node = enqueued.poll()) != null) {
|
||||
for (var neighbour : node.neighbours) {
|
||||
// Otherwise attempt to enqueue this neighbour as well.
|
||||
if (reachable.add(neighbour)) enqueued.add(neighbour);
|
||||
}
|
||||
}
|
||||
|
||||
return reachable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,39 @@ public final class WiredNodeImpl implements WiredNode {
|
||||
final HashSet<WiredNodeImpl> neighbours = new HashSet<>();
|
||||
volatile WiredNetworkImpl network;
|
||||
|
||||
/**
|
||||
* A temporary field used when checking network connectivity.
|
||||
*
|
||||
* @see WiredNetworkImpl#remove(WiredNode)
|
||||
*/
|
||||
@Nullable
|
||||
NodeSet currentSet;
|
||||
|
||||
public WiredNodeImpl(WiredElement element) {
|
||||
this.element = element;
|
||||
network = new WiredNetworkImpl(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean connectTo(WiredNode node) {
|
||||
return network.connect(this, node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean disconnectFrom(WiredNode node) {
|
||||
return network == ((WiredNodeImpl) node).network && network.disconnect(this, node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove() {
|
||||
return network.remove(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePeripherals(Map<String, IPeripheral> peripherals) {
|
||||
network.updatePeripherals(this, peripherals);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void addReceiver(PacketReceiver receiver) {
|
||||
if (receivers == null) receivers = new HashSet<>();
|
||||
|
||||
@@ -18,6 +18,7 @@ import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.dedicated.DedicatedServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.packs.resources.PreparableReloadListener;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
@@ -72,10 +73,19 @@ public final class CommonHooks {
|
||||
NetworkUtils.reset();
|
||||
}
|
||||
|
||||
public static void onServerChunkUnload(LevelChunk chunk) {
|
||||
if (!(chunk.getLevel() instanceof ServerLevel)) throw new IllegalArgumentException("Not a server chunk.");
|
||||
TickScheduler.onChunkUnload(chunk);
|
||||
}
|
||||
|
||||
public static void onChunkWatch(LevelChunk chunk, ServerPlayer player) {
|
||||
MonitorWatcher.onWatch(chunk, player);
|
||||
}
|
||||
|
||||
public static void onChunkTicketLevelChanged(ServerLevel level, long chunkPos, int oldLevel, int newLevel) {
|
||||
TickScheduler.onChunkTicketChanged(level, chunkPos, oldLevel, newLevel);
|
||||
}
|
||||
|
||||
public static final ResourceLocation TREASURE_DISK_LOOT = new ResourceLocation(ComputerCraftAPI.MOD_ID, "treasure_disk");
|
||||
|
||||
private static final Set<ResourceLocation> TREASURE_DISK_LOOT_TABLES = Set.of(
|
||||
|
||||
@@ -113,16 +113,13 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
public void neighborChanged(BlockPos neighbour) {
|
||||
updateInputAt(neighbour);
|
||||
}
|
||||
|
||||
protected void serverTick() {
|
||||
if (getLevel().isClientSide) return;
|
||||
if (computerID < 0 && !startOn) return; // Don't tick if we don't need a computer!
|
||||
|
||||
var computer = createServerComputer();
|
||||
|
||||
// Update any peripherals that have changed.
|
||||
if (invalidSides != 0) {
|
||||
for (var direction : DirectionUtil.FACINGS) {
|
||||
if ((invalidSides & (1 << direction.ordinal())) != 0) refreshPeripheral(computer, direction);
|
||||
@@ -139,16 +136,30 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
|
||||
|
||||
fresh = false;
|
||||
computerID = computer.getID();
|
||||
label = computer.getLabel();
|
||||
on = computer.isOn();
|
||||
|
||||
// Update the block state if needed. We don't fire a block update intentionally,
|
||||
// as this only really is needed on the client side.
|
||||
// If the on state has changed, mark as as dirty.
|
||||
var newOn = computer.isOn();
|
||||
if (on != newOn) {
|
||||
on = newOn;
|
||||
setChanged();
|
||||
}
|
||||
|
||||
// If the label has changed, mark as dirty and sync to client.
|
||||
var newLabel = computer.getLabel();
|
||||
if (!Objects.equals(label, newLabel)) {
|
||||
label = newLabel;
|
||||
BlockEntityHelpers.updateBlock(this);
|
||||
}
|
||||
|
||||
// Update the block state if needed.
|
||||
updateBlockState(computer.getState());
|
||||
|
||||
// TODO: This should ideally be split up into label/id/on (which should save NBT and sync to client) and
|
||||
// redstone (which should update outputs)
|
||||
if (computer.hasOutputChanged()) updateOutput();
|
||||
var changes = computer.pollAndResetChanges();
|
||||
if (changes != 0) {
|
||||
for (var direction : DirectionUtil.FACINGS) {
|
||||
if ((changes & (1 << remapToLocalSide(direction).ordinal())) != 0) updateRedstoneTo(direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void updateBlockState(ComputerState newState);
|
||||
@@ -198,11 +209,15 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
|
||||
return localSide;
|
||||
}
|
||||
|
||||
private void updateRedstoneInputs(ServerComputer computer) {
|
||||
var pos = getBlockPos();
|
||||
for (var dir : DirectionUtil.FACINGS) updateRedstoneInput(computer, dir, pos.relative(dir));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the redstone input on a particular side.
|
||||
* <p>
|
||||
* This is called <em>immediately</em> when a neighbouring block changes (see {@link #neighborChanged(BlockPos)}).
|
||||
*
|
||||
* @param computer The current server computer.
|
||||
* @param dir The direction to update in.
|
||||
* @param targetPos The position of the adjacent block, equal to {@code getBlockPos().offset(dir)}.
|
||||
*/
|
||||
private void updateRedstoneInput(ServerComputer computer, Direction dir, BlockPos targetPos) {
|
||||
var offsetSide = dir.getOpposite();
|
||||
var localDir = remapToLocalSide(dir);
|
||||
@@ -211,6 +226,15 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
|
||||
computer.setBundledRedstoneInput(localDir, BundledRedstone.getOutput(getLevel(), targetPos, offsetSide));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the peripheral on a particular side.
|
||||
* <p>
|
||||
* This is called from {@link #serverTick()}, after a peripheral has been marked as invalid (such as in
|
||||
* {@link #neighborChanged(BlockPos)})
|
||||
*
|
||||
* @param computer The current server computer.
|
||||
* @param dir The direction to update in.
|
||||
*/
|
||||
private void refreshPeripheral(ServerComputer computer, Direction dir) {
|
||||
invalidSides &= ~(1 << dir.ordinal());
|
||||
|
||||
@@ -243,7 +267,18 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
|
||||
}
|
||||
}
|
||||
|
||||
private void updateInputAt(BlockPos neighbour) {
|
||||
/**
|
||||
* Called when a neighbour block changes.
|
||||
* <p>
|
||||
* This finds the side the neighbour block is on, and updates the inputs accordingly.
|
||||
* <p>
|
||||
* We do <strong>NOT</strong> update the peripheral immediately. Blocks and block entities are sometimes
|
||||
* inconsistent at the point where an update is received, and so we instead just mark that side as dirty (see
|
||||
* {@link #invalidSides}) and refresh it {@linkplain #serverTick() next tick}.
|
||||
*
|
||||
* @param neighbour The position of the neighbour block.
|
||||
*/
|
||||
public void neighborChanged(BlockPos neighbour) {
|
||||
var computer = getServerComputer();
|
||||
if (computer == null) return;
|
||||
|
||||
@@ -258,22 +293,28 @@ 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);
|
||||
for (var dir : DirectionUtil.FACINGS) updateRedstoneInput(computer, dir, getBlockPos().relative(dir));
|
||||
invalidSides = (1 << 6) - 1; // Mark all peripherals as dirty.
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the block's state and propagate redstone output.
|
||||
* Update outputs in a specific direction.
|
||||
*
|
||||
* @param direction The direction to propagate outputs in.
|
||||
*/
|
||||
public void updateOutput() {
|
||||
BlockEntityHelpers.updateBlock(this);
|
||||
for (var dir : DirectionUtil.FACINGS) RedstoneUtil.propagateRedstoneOutput(getLevel(), getBlockPos(), dir);
|
||||
protected void updateRedstoneTo(Direction direction) {
|
||||
RedstoneUtil.propagateRedstoneOutput(getLevel(), getBlockPos(), direction);
|
||||
|
||||
var computer = getServerComputer();
|
||||
if (computer != null) updateRedstoneInputs(computer);
|
||||
if (computer != null) updateRedstoneInput(computer, direction, getBlockPos().relative(direction));
|
||||
}
|
||||
|
||||
protected abstract ServerComputer createComputer(int id);
|
||||
/**
|
||||
* Update all redstone outputs.
|
||||
*/
|
||||
public void updateRedstone() {
|
||||
for (var dir : DirectionUtil.FACINGS) updateRedstoneTo(dir);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int getComputerID() {
|
||||
@@ -331,6 +372,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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,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 +95,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
|
||||
public void tickServer() {
|
||||
ticksSincePing++;
|
||||
|
||||
computer.tick();
|
||||
|
||||
changedLastFrame = computer.pollAndResetChanged();
|
||||
if (terminalChanged.getAndSet(false)) onTerminalChanged();
|
||||
}
|
||||
|
||||
@@ -119,8 +115,8 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
return ticksSincePing > 100;
|
||||
}
|
||||
|
||||
public boolean hasOutputChanged() {
|
||||
return changedLastFrame;
|
||||
public int pollAndResetChanges() {
|
||||
return computer.pollAndResetChanges();
|
||||
}
|
||||
|
||||
public int register() {
|
||||
@@ -167,7 +163,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ 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();
|
||||
|
||||
@@ -264,8 +264,8 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ 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);
|
||||
|
||||
|
||||
@@ -15,7 +15,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;
|
||||
@@ -86,11 +85,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 +94,7 @@ public final class WiredModemLocalPeripheral {
|
||||
}
|
||||
|
||||
public Map<String, IPeripheral> toMap() {
|
||||
return peripheral == null
|
||||
? Map.of()
|
||||
: Collections.singletonMap(type + "_" + id, peripheral);
|
||||
return peripheral == null ? Map.of() : Map.of(type + "_" + id, peripheral);
|
||||
}
|
||||
|
||||
public void write(CompoundTag tag, String suffix) {
|
||||
|
||||
@@ -45,13 +45,18 @@ public class MonitorBlockEntity extends BlockEntity {
|
||||
private final boolean advanced;
|
||||
|
||||
private @Nullable ServerMonitor serverMonitor;
|
||||
|
||||
/**
|
||||
* The monitor's state on the client. This is defined iff we're the origin monitor
|
||||
* ({@code xIndex == 0 && yIndex == 0}).
|
||||
*/
|
||||
private @Nullable ClientMonitor clientMonitor;
|
||||
|
||||
private @Nullable MonitorPeripheral peripheral;
|
||||
private final Set<IComputerAccess> computers = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
|
||||
private boolean needsUpdate = false;
|
||||
private boolean needsValidating = false;
|
||||
private boolean destroyed = false;
|
||||
|
||||
// MonitorWatcher state.
|
||||
boolean enqueued;
|
||||
@@ -90,7 +95,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
|
||||
@@ -144,7 +149,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;
|
||||
@@ -183,13 +188,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
|
||||
@@ -210,17 +213,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) {
|
||||
@@ -287,7 +287,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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -310,8 +310,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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -390,7 +390,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();
|
||||
}
|
||||
|
||||
@@ -560,7 +560,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++) {
|
||||
|
||||
@@ -11,6 +11,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;
|
||||
@@ -37,7 +38,10 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
|
||||
private ItemStack stack = ItemStack.EMPTY;
|
||||
|
||||
private int lightColour = -1;
|
||||
private boolean lightChanged = false;
|
||||
|
||||
// The state the previous tick, used to determine if the state needs to be sent to the client.
|
||||
private int oldLightColour = -1;
|
||||
private @Nullable ComputerState oldComputerState;
|
||||
|
||||
private final Set<ServerPlayer> tracking = new HashSet<>();
|
||||
|
||||
@@ -82,10 +86,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
|
||||
@@ -156,9 +157,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);
|
||||
|
||||
@@ -186,7 +186,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();
|
||||
|
||||
@@ -296,7 +296,7 @@ public class TurtleBrain implements TurtleAccessInternal {
|
||||
oldWorld.removeBlock(oldPos, false);
|
||||
|
||||
// Make sure everybody knows about it
|
||||
newTurtle.updateOutput();
|
||||
newTurtle.updateRedstone();
|
||||
newTurtle.updateInputsImmediately();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -5,13 +5,20 @@
|
||||
package dan200.computercraft.shared.util;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ChunkLevel;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelAccessor;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.chunk.ChunkStatus;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
|
||||
import java.util.Queue;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
|
||||
|
||||
/**
|
||||
* A thread-safe version of {@link LevelAccessor#scheduleTick(BlockPos, Block, int)}.
|
||||
@@ -22,26 +29,92 @@ public final class TickScheduler {
|
||||
private TickScheduler() {
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of block entities to tick.
|
||||
*/
|
||||
private static final Queue<Token> toTick = new ConcurrentLinkedDeque<>();
|
||||
|
||||
/**
|
||||
* Block entities which we want to tick, but whose chunks not currently loaded.
|
||||
* <p>
|
||||
* Minecraft sometimes keeps chunks in-memory, but not actively loaded. If such a block entity is in the
|
||||
* {@link #toTick} queue, we'll see that it's not loaded and so have to skip scheduling a tick.
|
||||
* <p>
|
||||
* However, if the block entity is ever loaded again, we need to tick it. Unfortunately, block entities in this
|
||||
* state are not notified in any way (for instance, {@link BlockEntity#setRemoved()} or
|
||||
* {@link BlockEntity#clearRemoved()} are not called), and so there's no way to easily reschedule them for ticking.
|
||||
* <p>
|
||||
* Instead, for each chunk we keep a list of all block entities whose tick we skipped. If a chunk is loaded,
|
||||
* {@linkplain #onChunkTicketChanged(ServerLevel, long, int, int) we requeue all skipped ticks}.
|
||||
*/
|
||||
private static final Map<ChunkReference, List<Token>> delayed = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Schedule a block entity to be ticked.
|
||||
*
|
||||
* @param token The token whose block entity should be ticked.
|
||||
*/
|
||||
public static void schedule(Token token) {
|
||||
var world = token.owner.getLevel();
|
||||
if (world != null && !world.isClientSide && !token.scheduled.getAndSet(true)) toTick.add(token);
|
||||
if (world != null && !world.isClientSide && Token.STATE.compareAndSet(token, State.IDLE, State.SCHEDULED)) {
|
||||
toTick.add(token);
|
||||
}
|
||||
}
|
||||
|
||||
public static void onChunkTicketChanged(ServerLevel level, long chunkPos, int oldLevel, int newLevel) {
|
||||
boolean oldLoaded = isLoaded(oldLevel), newLoaded = isLoaded(newLevel);
|
||||
if (!oldLoaded && newLoaded) {
|
||||
// If our chunk is becoming active, requeue all pending tokens.
|
||||
var delayedTokens = delayed.remove(new ChunkReference(level.dimension(), chunkPos));
|
||||
if (delayedTokens == null) return;
|
||||
|
||||
for (var token : delayedTokens) {
|
||||
if (token.owner.isRemoved()) {
|
||||
Token.STATE.set(token, State.IDLE);
|
||||
} else {
|
||||
Token.STATE.set(token, State.SCHEDULED);
|
||||
toTick.add(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void onChunkUnload(LevelChunk chunk) {
|
||||
// If our chunk is fully unloaded, all block entities are about to be removed - we need to dequeue any delayed
|
||||
// tokens from the queue.
|
||||
var delayedTokens = delayed.remove(new ChunkReference(chunk.getLevel().dimension(), chunk.getPos().toLong()));
|
||||
if (delayedTokens == null) return;
|
||||
|
||||
for (var token : delayedTokens) Token.STATE.set(token, State.IDLE);
|
||||
}
|
||||
|
||||
public static void tick() {
|
||||
Token token;
|
||||
while ((token = toTick.poll()) != null) {
|
||||
token.scheduled.set(false);
|
||||
var blockEntity = token.owner;
|
||||
if (blockEntity.isRemoved()) continue;
|
||||
while ((token = toTick.poll()) != null) Token.STATE.set(token, tickToken(token));
|
||||
}
|
||||
|
||||
var world = blockEntity.getLevel();
|
||||
var pos = blockEntity.getBlockPos();
|
||||
private static State tickToken(Token token) {
|
||||
var blockEntity = token.owner;
|
||||
|
||||
if (world != null && world.isLoaded(pos) && world.getBlockEntity(pos) == blockEntity) {
|
||||
world.scheduleTick(pos, blockEntity.getBlockState().getBlock(), 0);
|
||||
// If the block entity has been removed, then remove it from the queue.
|
||||
if (blockEntity.isRemoved()) return State.IDLE;
|
||||
|
||||
var level = Objects.requireNonNull(blockEntity.getLevel(), "Block entity level cannot become null");
|
||||
var pos = blockEntity.getBlockPos();
|
||||
|
||||
if (!level.isLoaded(pos)) {
|
||||
// The chunk is not properly loaded, as it to our delayed set.
|
||||
delayed.computeIfAbsent(new ChunkReference(level.dimension(), ChunkPos.asLong(pos)), x -> new ArrayList<>()).add(token);
|
||||
return State.UNLOADED;
|
||||
} else {
|
||||
// This should be impossible: either the block entity is at the above position, or it has been removed.
|
||||
if (level.getBlockEntity(pos) != blockEntity) {
|
||||
throw new IllegalStateException("Expected " + blockEntity + " at " + pos);
|
||||
}
|
||||
|
||||
// Otherwise schedule a tick and remove it from the queue.
|
||||
level.scheduleTick(pos, blockEntity.getBlockState().getBlock(), 0);
|
||||
return State.IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,11 +125,51 @@ public final class TickScheduler {
|
||||
* As such, it should be unique per {@link BlockEntity} instance to avoid it being queued multiple times.
|
||||
*/
|
||||
public static class Token {
|
||||
static final AtomicReferenceFieldUpdater<Token, State> STATE = AtomicReferenceFieldUpdater.newUpdater(Token.class, State.class, "$state");
|
||||
|
||||
final BlockEntity owner;
|
||||
final AtomicBoolean scheduled = new AtomicBoolean();
|
||||
|
||||
/**
|
||||
* The current state of this token.
|
||||
*/
|
||||
private volatile State $state = State.IDLE;
|
||||
|
||||
public Token(BlockEntity owner) {
|
||||
this.owner = owner;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The possible states a {@link Token} can be in.
|
||||
* <p>
|
||||
* This effectively stores which (if any) queue the token is currently in, allowing us to skip scheduling if the
|
||||
* token is already enqueued.
|
||||
*/
|
||||
private enum State {
|
||||
/**
|
||||
* The token is not on any queues.
|
||||
*/
|
||||
IDLE,
|
||||
|
||||
/**
|
||||
* The token is on the {@link #toTick} queue.
|
||||
*/
|
||||
SCHEDULED,
|
||||
|
||||
/**
|
||||
* The token is on the {@link #delayed} queue.
|
||||
*/
|
||||
UNLOADED,
|
||||
}
|
||||
|
||||
private record ChunkReference(ResourceKey<Level> level, Long position) {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChunkReference(" + level + " at " + new ChunkPos(position) + ")";
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isLoaded(int level) {
|
||||
return level <= ChunkLevel.byStatus(ChunkStatus.FULL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.impl.network.wired;
|
||||
|
||||
import dan200.computercraft.api.network.wired.WiredNetwork;
|
||||
import dan200.computercraft.impl.network.wired.NetworkTest.NetworkElement;
|
||||
import dan200.computercraft.shared.util.DirectionUtil;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import org.openjdk.jmh.annotations.*;
|
||||
import org.openjdk.jmh.runner.Runner;
|
||||
import org.openjdk.jmh.runner.RunnerException;
|
||||
import org.openjdk.jmh.runner.options.OptionsBuilder;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
|
||||
@BenchmarkMode(Mode.AverageTime)
|
||||
@OutputTimeUnit(TimeUnit.MILLISECONDS)
|
||||
public class NetworkBenchmark {
|
||||
private static final int BRUTE_SIZE = 16;
|
||||
|
||||
public static void main(String[] args) throws RunnerException {
|
||||
var opts = new OptionsBuilder()
|
||||
.include(NetworkBenchmark.class.getName() + "\\..*")
|
||||
.warmupIterations(2)
|
||||
.measurementIterations(5)
|
||||
.forks(1)
|
||||
.build();
|
||||
new Runner(opts).run();
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
@Warmup(time = 1, timeUnit = TimeUnit.SECONDS)
|
||||
@Measurement(time = 2, timeUnit = TimeUnit.SECONDS)
|
||||
public void removeEveryNode(ConnectedGrid grid) {
|
||||
grid.grid.forEach((node, pos) -> node.remove());
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
@Warmup(time = 1, timeUnit = TimeUnit.SECONDS)
|
||||
@Measurement(time = 2, timeUnit = TimeUnit.SECONDS)
|
||||
public void connectAndDisconnect(SplitGrid connectedGrid) {
|
||||
WiredNodeImpl left = connectedGrid.left, right = connectedGrid.right;
|
||||
|
||||
assertNotEquals(left.getNetwork(), right.getNetwork());
|
||||
left.connectTo(right);
|
||||
assertEquals(left.getNetwork(), right.getNetwork());
|
||||
left.disconnectFrom(right);
|
||||
assertNotEquals(left.getNetwork(), right.getNetwork());
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
@Warmup(time = 1, timeUnit = TimeUnit.SECONDS)
|
||||
@Measurement(time = 2, timeUnit = TimeUnit.SECONDS)
|
||||
public void connectAndRemove(SplitGrid connectedGrid) {
|
||||
WiredNodeImpl left = connectedGrid.left, right = connectedGrid.right, centre = connectedGrid.centre;
|
||||
|
||||
assertNotEquals(left.getNetwork(), right.getNetwork());
|
||||
centre.connectTo(left);
|
||||
centre.connectTo(right);
|
||||
assertEquals(left.getNetwork(), right.getNetwork());
|
||||
centre.remove();
|
||||
assertNotEquals(left.getNetwork(), right.getNetwork());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a grid where all nodes are connected to their neighbours.
|
||||
*/
|
||||
@State(Scope.Thread)
|
||||
public static class ConnectedGrid {
|
||||
Grid<WiredNodeImpl> grid;
|
||||
|
||||
@Setup(Level.Invocation)
|
||||
public void setup() {
|
||||
var grid = this.grid = new Grid<>(BRUTE_SIZE);
|
||||
grid.map((existing, pos) -> new NetworkElement("n_" + pos, pos.getX() == pos.getY() && pos.getY() == pos.getZ()).getNode());
|
||||
|
||||
// Connect every node
|
||||
grid.forEach((node, pos) -> {
|
||||
for (var facing : DirectionUtil.FACINGS) {
|
||||
var other = grid.get(pos.relative(facing));
|
||||
if (other != null) node.connectTo(other);
|
||||
}
|
||||
});
|
||||
|
||||
var networks = countNetworks(grid);
|
||||
if (networks.size() != 1) throw new AssertionError("Expected exactly one network.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a grid where the nodes at {@code x < BRUTE_SIZE/2} and {@code x >= BRUTE_SIZE/2} are in separate networks,
|
||||
* but otherwise connected to their neighbours.
|
||||
*/
|
||||
@State(Scope.Thread)
|
||||
public static class SplitGrid {
|
||||
Grid<WiredNodeImpl> grid;
|
||||
WiredNodeImpl left, right, centre;
|
||||
|
||||
@Setup
|
||||
public void setup() {
|
||||
var grid = this.grid = new Grid<>(BRUTE_SIZE);
|
||||
grid.map((existing, pos) -> new NetworkElement("n_" + pos, pos.getX() == pos.getY() && pos.getY() == pos.getZ()).getNode());
|
||||
|
||||
// Connect every node
|
||||
grid.forEach((node, pos) -> {
|
||||
for (var facing : DirectionUtil.FACINGS) {
|
||||
var offset = pos.relative(facing);
|
||||
if (offset.getX() >= BRUTE_SIZE / 2 == pos.getX() >= BRUTE_SIZE / 2) {
|
||||
var other = grid.get(offset);
|
||||
if (other != null) node.connectTo(other);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var networks = countNetworks(grid);
|
||||
if (networks.size() != 2) throw new AssertionError("Expected exactly two networks.");
|
||||
for (var network : networks.object2IntEntrySet()) {
|
||||
if (network.getIntValue() != BRUTE_SIZE * BRUTE_SIZE * (BRUTE_SIZE / 2)) {
|
||||
throw new AssertionError("Network is the wrong size");
|
||||
}
|
||||
}
|
||||
|
||||
left = Objects.requireNonNull(grid.get(new BlockPos(BRUTE_SIZE / 2 - 1, 0, 0)));
|
||||
right = Objects.requireNonNull(grid.get(new BlockPos(BRUTE_SIZE / 2, 0, 0)));
|
||||
centre = new NetworkElement("c", false).getNode();
|
||||
}
|
||||
}
|
||||
|
||||
private static Object2IntMap<WiredNetwork> countNetworks(Grid<WiredNodeImpl> grid) {
|
||||
Object2IntMap<WiredNetwork> networks = new Object2IntOpenHashMap<>();
|
||||
grid.forEach((node, pos) -> networks.put(node.network, networks.getOrDefault(node.network, 0) + 1));
|
||||
return networks;
|
||||
}
|
||||
|
||||
private static class Grid<T> {
|
||||
private final int size;
|
||||
private final T[] box;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Grid(int size) {
|
||||
this.size = size;
|
||||
this.box = (T[]) new Object[size * size * size];
|
||||
}
|
||||
|
||||
public T get(BlockPos pos) {
|
||||
int x = pos.getX(), y = pos.getY(), z = pos.getZ();
|
||||
|
||||
return x >= 0 && x < size && y >= 0 && y < size && z >= 0 && z < size
|
||||
? box[x * size * size + y * size + z]
|
||||
: null;
|
||||
}
|
||||
|
||||
public void forEach(BiConsumer<T, BlockPos> transform) {
|
||||
for (var x = 0; x < size; x++) {
|
||||
for (var y = 0; y < size; y++) {
|
||||
for (var z = 0; z < size; z++) {
|
||||
transform.accept(box[x * size * size + y * size + z], new BlockPos(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void map(BiFunction<T, BlockPos, T> transform) {
|
||||
for (var x = 0; x < size; x++) {
|
||||
for (var y = 0; y < size; y++) {
|
||||
for (var z = 0; z < size; z++) {
|
||||
box[x * size * size + y * size + z] = transform.apply(box[x * size * size + y * size + z], new BlockPos(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,19 +9,14 @@ import dan200.computercraft.api.network.wired.WiredNetwork;
|
||||
import dan200.computercraft.api.network.wired.WiredNetworkChange;
|
||||
import dan200.computercraft.api.network.wired.WiredNode;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.shared.util.DirectionUtil;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@@ -29,11 +24,11 @@ public class NetworkTest {
|
||||
@Test
|
||||
public void testConnect() {
|
||||
NetworkElement
|
||||
aE = new NetworkElement(null, null, "a"),
|
||||
bE = new NetworkElement(null, null, "b"),
|
||||
cE = new NetworkElement(null, null, "c");
|
||||
aE = new NetworkElement("a"),
|
||||
bE = new NetworkElement("b"),
|
||||
cE = new NetworkElement("c");
|
||||
|
||||
WiredNode
|
||||
WiredNodeImpl
|
||||
aN = aE.getNode(),
|
||||
bN = bE.getNode(),
|
||||
cN = cE.getNode();
|
||||
@@ -42,8 +37,8 @@ public class NetworkTest {
|
||||
assertNotEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be different");
|
||||
assertNotEquals(bN.getNetwork(), cN.getNetwork(), "B's and C's network must be different");
|
||||
|
||||
assertTrue(aN.getNetwork().connect(aN, bN), "Must be able to add connection");
|
||||
assertFalse(aN.getNetwork().connect(aN, bN), "Cannot add connection twice");
|
||||
assertTrue(aN.connectTo(bN), "Must be able to add connection");
|
||||
assertFalse(aN.connectTo(bN), "Cannot add connection twice");
|
||||
|
||||
assertEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must be equal");
|
||||
assertEquals(Set.of(aN, bN), nodes(aN.getNetwork()), "A's network should be A and B");
|
||||
@@ -51,7 +46,7 @@ public class NetworkTest {
|
||||
assertEquals(Set.of("a", "b"), aE.allPeripherals().keySet(), "A's peripheral set should be A, B");
|
||||
assertEquals(Set.of("a", "b"), bE.allPeripherals().keySet(), "B's peripheral set should be A, B");
|
||||
|
||||
aN.getNetwork().connect(aN, cN);
|
||||
aN.connectTo(cN);
|
||||
|
||||
assertEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must be equal");
|
||||
assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal");
|
||||
@@ -69,20 +64,20 @@ public class NetworkTest {
|
||||
@Test
|
||||
public void testDisconnectNoChange() {
|
||||
NetworkElement
|
||||
aE = new NetworkElement(null, null, "a"),
|
||||
bE = new NetworkElement(null, null, "b"),
|
||||
cE = new NetworkElement(null, null, "c");
|
||||
aE = new NetworkElement("a"),
|
||||
bE = new NetworkElement("b"),
|
||||
cE = new NetworkElement("c");
|
||||
|
||||
WiredNode
|
||||
WiredNodeImpl
|
||||
aN = aE.getNode(),
|
||||
bN = bE.getNode(),
|
||||
cN = cE.getNode();
|
||||
|
||||
aN.getNetwork().connect(aN, bN);
|
||||
aN.getNetwork().connect(aN, cN);
|
||||
aN.getNetwork().connect(bN, cN);
|
||||
aN.connectTo(bN);
|
||||
aN.connectTo(cN);
|
||||
bN.connectTo(cN);
|
||||
|
||||
aN.getNetwork().disconnect(aN, bN);
|
||||
aN.disconnectFrom(bN);
|
||||
|
||||
assertEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must be equal");
|
||||
assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal");
|
||||
@@ -96,19 +91,19 @@ public class NetworkTest {
|
||||
@Test
|
||||
public void testDisconnectLeaf() {
|
||||
NetworkElement
|
||||
aE = new NetworkElement(null, null, "a"),
|
||||
bE = new NetworkElement(null, null, "b"),
|
||||
cE = new NetworkElement(null, null, "c");
|
||||
aE = new NetworkElement("a"),
|
||||
bE = new NetworkElement("b"),
|
||||
cE = new NetworkElement("c");
|
||||
|
||||
WiredNode
|
||||
WiredNodeImpl
|
||||
aN = aE.getNode(),
|
||||
bN = bE.getNode(),
|
||||
cN = cE.getNode();
|
||||
|
||||
aN.getNetwork().connect(aN, bN);
|
||||
aN.getNetwork().connect(aN, cN);
|
||||
aN.connectTo(bN);
|
||||
aN.connectTo(cN);
|
||||
|
||||
aN.getNetwork().disconnect(aN, bN);
|
||||
aN.disconnectFrom(bN);
|
||||
|
||||
assertNotEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal");
|
||||
assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal");
|
||||
@@ -123,23 +118,23 @@ public class NetworkTest {
|
||||
@Test
|
||||
public void testDisconnectSplit() {
|
||||
NetworkElement
|
||||
aE = new NetworkElement(null, null, "a"),
|
||||
aaE = new NetworkElement(null, null, "a_"),
|
||||
bE = new NetworkElement(null, null, "b"),
|
||||
bbE = new NetworkElement(null, null, "b_");
|
||||
aE = new NetworkElement("a"),
|
||||
aaE = new NetworkElement("a_"),
|
||||
bE = new NetworkElement("b"),
|
||||
bbE = new NetworkElement("b_");
|
||||
|
||||
WiredNode
|
||||
WiredNodeImpl
|
||||
aN = aE.getNode(),
|
||||
aaN = aaE.getNode(),
|
||||
bN = bE.getNode(),
|
||||
bbN = bbE.getNode();
|
||||
|
||||
aN.getNetwork().connect(aN, aaN);
|
||||
bN.getNetwork().connect(bN, bbN);
|
||||
aN.connectTo(aaN);
|
||||
bN.connectTo(bbN);
|
||||
|
||||
aN.getNetwork().connect(aN, bN);
|
||||
aN.connectTo(bN);
|
||||
|
||||
aN.getNetwork().disconnect(aN, bN);
|
||||
aN.disconnectFrom(bN);
|
||||
|
||||
assertNotEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal");
|
||||
assertEquals(aN.getNetwork(), aaN.getNetwork(), "A's and A_'s network must be equal");
|
||||
@@ -154,7 +149,7 @@ public class NetworkTest {
|
||||
|
||||
@Test
|
||||
public void testRemoveSingle() {
|
||||
var aE = new NetworkElement(null, null, "a");
|
||||
var aE = new NetworkElement("a");
|
||||
var aN = aE.getNode();
|
||||
|
||||
var network = aN.getNetwork();
|
||||
@@ -165,20 +160,20 @@ public class NetworkTest {
|
||||
@Test
|
||||
public void testRemoveLeaf() {
|
||||
NetworkElement
|
||||
aE = new NetworkElement(null, null, "a"),
|
||||
bE = new NetworkElement(null, null, "b"),
|
||||
cE = new NetworkElement(null, null, "c");
|
||||
aE = new NetworkElement("a"),
|
||||
bE = new NetworkElement("b"),
|
||||
cE = new NetworkElement("c");
|
||||
|
||||
WiredNode
|
||||
WiredNodeImpl
|
||||
aN = aE.getNode(),
|
||||
bN = bE.getNode(),
|
||||
cN = cE.getNode();
|
||||
|
||||
aN.getNetwork().connect(aN, bN);
|
||||
aN.getNetwork().connect(aN, cN);
|
||||
aN.connectTo(bN);
|
||||
aN.connectTo(cN);
|
||||
|
||||
assertTrue(aN.getNetwork().remove(bN), "Must be able to remove node");
|
||||
assertFalse(aN.getNetwork().remove(bN), "Cannot remove a second time");
|
||||
assertTrue(bN.remove(), "Must be able to remove node");
|
||||
assertFalse(bN.remove(), "Cannot remove a second time");
|
||||
|
||||
assertNotEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal");
|
||||
assertEquals(aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal");
|
||||
@@ -194,26 +189,26 @@ public class NetworkTest {
|
||||
@Test
|
||||
public void testRemoveSplit() {
|
||||
NetworkElement
|
||||
aE = new NetworkElement(null, null, "a"),
|
||||
aaE = new NetworkElement(null, null, "a_"),
|
||||
bE = new NetworkElement(null, null, "b"),
|
||||
bbE = new NetworkElement(null, null, "b_"),
|
||||
cE = new NetworkElement(null, null, "c");
|
||||
aE = new NetworkElement("a"),
|
||||
aaE = new NetworkElement("a_"),
|
||||
bE = new NetworkElement("b"),
|
||||
bbE = new NetworkElement("b_"),
|
||||
cE = new NetworkElement("c");
|
||||
|
||||
WiredNode
|
||||
WiredNodeImpl
|
||||
aN = aE.getNode(),
|
||||
aaN = aaE.getNode(),
|
||||
bN = bE.getNode(),
|
||||
bbN = bbE.getNode(),
|
||||
cN = cE.getNode();
|
||||
|
||||
aN.getNetwork().connect(aN, aaN);
|
||||
bN.getNetwork().connect(bN, bbN);
|
||||
aN.connectTo(aaN);
|
||||
bN.connectTo(bbN);
|
||||
|
||||
cN.getNetwork().connect(aN, cN);
|
||||
cN.getNetwork().connect(bN, cN);
|
||||
cN.connectTo(aN);
|
||||
cN.connectTo(bN);
|
||||
|
||||
cN.getNetwork().remove(cN);
|
||||
cN.remove();
|
||||
|
||||
assertNotEquals(aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal");
|
||||
assertEquals(aN.getNetwork(), aaN.getNetwork(), "A's and A_'s network must be equal");
|
||||
@@ -228,96 +223,30 @@ public class NetworkTest {
|
||||
assertEquals(Set.of(), cE.allPeripherals().keySet(), "C's peripheral set should be empty");
|
||||
}
|
||||
|
||||
private static final int BRUTE_SIZE = 16;
|
||||
private static final int TOGGLE_CONNECTION_TIMES = 5;
|
||||
private static final int TOGGLE_NODE_TIMES = 5;
|
||||
|
||||
@Test
|
||||
@Disabled("Takes a long time to run, mostly for stress testing")
|
||||
public void testLarge() {
|
||||
var grid = new Grid<WiredNode>(BRUTE_SIZE);
|
||||
grid.map((existing, pos) -> new NetworkElement(null, null, "n_" + pos).getNode());
|
||||
|
||||
// Test connecting
|
||||
{
|
||||
var start = System.nanoTime();
|
||||
|
||||
grid.forEach((existing, pos) -> {
|
||||
for (var facing : DirectionUtil.FACINGS) {
|
||||
var offset = pos.relative(facing);
|
||||
if (offset.getX() > BRUTE_SIZE / 2 == pos.getX() > BRUTE_SIZE / 2) {
|
||||
var other = grid.get(offset);
|
||||
if (other != null) existing.getNetwork().connect(existing, other);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var end = System.nanoTime();
|
||||
|
||||
System.out.printf("Connecting %s³ nodes took %s seconds\n", BRUTE_SIZE, (end - start) * 1e-9);
|
||||
}
|
||||
|
||||
// Test toggling
|
||||
{
|
||||
var left = grid.get(new BlockPos(BRUTE_SIZE / 2, 0, 0));
|
||||
var right = grid.get(new BlockPos(BRUTE_SIZE / 2 + 1, 0, 0));
|
||||
assertNotEquals(left.getNetwork(), right.getNetwork());
|
||||
|
||||
var start = System.nanoTime();
|
||||
for (var i = 0; i < TOGGLE_CONNECTION_TIMES; i++) {
|
||||
left.getNetwork().connect(left, right);
|
||||
left.getNetwork().disconnect(left, right);
|
||||
}
|
||||
|
||||
var end = System.nanoTime();
|
||||
|
||||
System.out.printf("Toggling connection %s times took %s seconds\n", TOGGLE_CONNECTION_TIMES, (end - start) * 1e-9);
|
||||
}
|
||||
|
||||
{
|
||||
var left = grid.get(new BlockPos(BRUTE_SIZE / 2, 0, 0));
|
||||
var right = grid.get(new BlockPos(BRUTE_SIZE / 2 + 1, 0, 0));
|
||||
var centre = new NetworkElement(null, null, "c").getNode();
|
||||
assertNotEquals(left.getNetwork(), right.getNetwork());
|
||||
|
||||
var start = System.nanoTime();
|
||||
for (var i = 0; i < TOGGLE_NODE_TIMES; i++) {
|
||||
left.getNetwork().connect(left, centre);
|
||||
right.getNetwork().connect(right, centre);
|
||||
|
||||
left.getNetwork().remove(centre);
|
||||
}
|
||||
|
||||
var end = System.nanoTime();
|
||||
|
||||
System.out.printf("Toggling node %s times took %s seconds\n", TOGGLE_NODE_TIMES, (end - start) * 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class NetworkElement implements WiredElement {
|
||||
private final Level world;
|
||||
private final Vec3 position;
|
||||
static final class NetworkElement implements WiredElement {
|
||||
private final String id;
|
||||
private final WiredNode node;
|
||||
private final WiredNodeImpl node;
|
||||
private final Map<String, IPeripheral> localPeripherals = new HashMap<>();
|
||||
private final Map<String, IPeripheral> remotePeripherals = new HashMap<>();
|
||||
|
||||
private NetworkElement(Level world, Vec3 position, String id) {
|
||||
this.world = world;
|
||||
this.position = position;
|
||||
NetworkElement(String id) {
|
||||
this(id, true);
|
||||
}
|
||||
|
||||
NetworkElement(String id, boolean peripheral) {
|
||||
this.id = id;
|
||||
this.node = new WiredNodeImpl(this);
|
||||
this.addPeripheral(id);
|
||||
if (peripheral) addPeripheral(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Level getLevel() {
|
||||
return world;
|
||||
throw new IllegalStateException("Unexpected call to getLevel()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 getPosition() {
|
||||
return position;
|
||||
throw new IllegalStateException("Unexpected call to getPosition()");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -331,7 +260,7 @@ public class NetworkTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public WiredNode getNode() {
|
||||
public WiredNodeImpl getNode() {
|
||||
return node;
|
||||
}
|
||||
|
||||
@@ -364,45 +293,6 @@ public class NetworkTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static class Grid<T> {
|
||||
private final int size;
|
||||
private final T[] box;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Grid(int size) {
|
||||
this.size = size;
|
||||
this.box = (T[]) new Object[size * size * size];
|
||||
}
|
||||
|
||||
public T get(BlockPos pos) {
|
||||
int x = pos.getX(), y = pos.getY(), z = pos.getZ();
|
||||
|
||||
return x >= 0 && x < size && y >= 0 && y < size && z >= 0 && z < size
|
||||
? box[x * size * size + y * size + z]
|
||||
: null;
|
||||
}
|
||||
|
||||
public void forEach(BiConsumer<T, BlockPos> transform) {
|
||||
for (var x = 0; x < size; x++) {
|
||||
for (var y = 0; y < size; y++) {
|
||||
for (var z = 0; z < size; z++) {
|
||||
transform.accept(box[x * size * size + y * size + z], new BlockPos(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void map(BiFunction<T, BlockPos, T> transform) {
|
||||
for (var x = 0; x < size; x++) {
|
||||
for (var y = 0; y < size; y++) {
|
||||
for (var z = 0; z < size; z++) {
|
||||
box[x * size * size + y * size + z] = transform.apply(box[x * size * size + y * size + z], new BlockPos(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<WiredNodeImpl> nodes(WiredNetwork network) {
|
||||
return ((WiredNetworkImpl) network).nodes;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -13,8 +13,54 @@ import dan200.computercraft.core.util.Colour;
|
||||
|
||||
|
||||
/**
|
||||
* Interact with a computer's terminal or monitors, writing text and drawing
|
||||
* ASCII graphics.
|
||||
* Interact with a computer's terminal or monitors, writing text and drawing ASCII graphics.
|
||||
*
|
||||
* <h2>Writing to the terminal</h2>
|
||||
* The simplest operation one can perform on a terminal is displaying (or writing) some text. This can be performed with
|
||||
* the [`term.write`] method.
|
||||
*
|
||||
* <pre>{@code
|
||||
* term.write("Hello, world!")
|
||||
* }</pre>
|
||||
* <p>
|
||||
* When you write text, this advances the cursor, so the next call to [`term.write`] will write text immediately after
|
||||
* the previous one.
|
||||
*
|
||||
* <pre>{@code
|
||||
* term.write("Hello, world!")
|
||||
* term.write("Some more text")
|
||||
* }</pre>
|
||||
* <p>
|
||||
* [`term.getCursorPos`] and [`term.setCursorPos`] can be used to manually change the cursor's position.
|
||||
* <p>
|
||||
* <pre>{@code
|
||||
* term.clear()
|
||||
*
|
||||
* term.setCursorPos(1, 1) -- The first column of line 1
|
||||
* term.write("First line")
|
||||
*
|
||||
* term.setCursorPos(20, 2) -- The 20th column of line 2
|
||||
* term.write("Second line")
|
||||
* }</pre>
|
||||
* <p>
|
||||
* [`term.write`] is a relatively basic and low-level function, and does not handle more advanced features such as line
|
||||
* breaks or word wrapping. If you just want to display text to the screen, you probably want to use [`print`] or
|
||||
* [`write`] instead.
|
||||
*
|
||||
* <h2>Colours</h2>
|
||||
* So far we've been writing text in black and white. However, advanced computers are also capable of displaying text
|
||||
* in a variety of colours, with the [`term.setTextColour`] and [`term.setBackgroundColour`] functions.
|
||||
*
|
||||
* <pre>{@code
|
||||
* print("This text is white")
|
||||
* term.setTextColour(colours.green)
|
||||
* print("This text is green")
|
||||
* }</pre>
|
||||
* <p>
|
||||
* These functions accept any of the constants from the [`colors`] API. [Combinations of colours][`colors.combine`] may
|
||||
* be accepted, but will only display a single colour (typically following the behaviour of [`colors.toBlit`]).
|
||||
* <p>
|
||||
* The [`paintutils`] API provides several helpful functions for displaying graphics using [`term.setBackgroundColour`].
|
||||
*
|
||||
* @cc.module term
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
package dan200.computercraft.core.apis.http.options;
|
||||
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
|
||||
/**
|
||||
* Options for a given HTTP request or websocket, which control its resource constraints.
|
||||
@@ -14,5 +15,6 @@ package dan200.computercraft.core.apis.http.options;
|
||||
* @param websocketMessage The maximum size of a websocket message (outgoing and incoming).
|
||||
* @param useProxy Whether to use the configured proxy.
|
||||
*/
|
||||
@Immutable
|
||||
public record Options(Action action, long maxUpload, long maxDownload, int websocketMessage, boolean useProxy) {
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package dan200.computercraft.core.apis.http.options;
|
||||
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
import com.google.errorprone.annotations.concurrent.LazyInit;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Optional;
|
||||
@@ -23,7 +24,7 @@ public final class PartialOptions {
|
||||
private final OptionalInt websocketMessage;
|
||||
private final Optional<Boolean> useProxy;
|
||||
|
||||
@SuppressWarnings("Immutable") // Lazily initialised, so this mutation is invisible in the public API
|
||||
@LazyInit
|
||||
private @Nullable Options options;
|
||||
|
||||
public PartialOptions(@Nullable Action action, OptionalLong maxUpload, OptionalLong maxDownload, OptionalInt websocketMessage, Optional<Boolean> useProxy) {
|
||||
|
||||
@@ -27,7 +27,7 @@ final class ResultHelpers {
|
||||
return throwUnchecked0(t);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@SuppressWarnings({ "unchecked", "TypeParameterUnusedInFormals" })
|
||||
private static <T extends Throwable> T throwUnchecked0(Throwable t) throws T {
|
||||
throw (T) t;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,7 @@ import dan200.computercraft.core.filesystem.FileSystem;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
@@ -54,9 +53,9 @@ public class Computer {
|
||||
private final AtomicLong lastTaskId = new AtomicLong();
|
||||
|
||||
// Additional state about the computer and its environment.
|
||||
private boolean blinking = false;
|
||||
private final Environment internalEnvironment;
|
||||
private final AtomicBoolean externalOutputChanged = new AtomicBoolean();
|
||||
|
||||
private final AtomicInteger externalOutputChanges = new AtomicInteger();
|
||||
|
||||
private boolean startRequested;
|
||||
private int ticksSinceStart = -1;
|
||||
@@ -140,10 +139,7 @@ public class Computer {
|
||||
}
|
||||
|
||||
public void setLabel(@Nullable String label) {
|
||||
if (!Objects.equals(label, this.label)) {
|
||||
this.label = label;
|
||||
externalOutputChanged.set(true);
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -222,22 +222,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
# 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.
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
New features in CC: Tweaked 1.109.6
|
||||
New features in CC: Tweaked 1.109.7
|
||||
|
||||
* Improve several Lua parser error messages.
|
||||
* Allow addon mods to register `require`able modules.
|
||||
* Improve performance of removing and unloading wired cables/modems.
|
||||
|
||||
Several bug fixes:
|
||||
* Fix weak tables becoming malformed when keys are GCed.
|
||||
* 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.
|
||||
|
||||
Type "help changelog" to see the full version history.
|
||||
|
||||
@@ -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<Integer, Integer> visitedLines) throws IOException {
|
||||
if (!Files.exists(fullName)) {
|
||||
LOG.error("Cannot locate file {}", fullName);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -6,20 +6,37 @@ package dan200.computercraft.mixin;
|
||||
|
||||
import dan200.computercraft.shared.CommonHooks;
|
||||
import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
|
||||
import net.minecraft.server.level.ChunkHolder;
|
||||
import net.minecraft.server.level.ChunkMap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import org.apache.commons.lang3.mutable.MutableObject;
|
||||
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.CallbackInfo;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@Mixin(ChunkMap.class)
|
||||
class ChunkMapMixin {
|
||||
@Final
|
||||
@Shadow
|
||||
ServerLevel level;
|
||||
|
||||
@Inject(method = "playerLoadedChunk", at = @At("TAIL"))
|
||||
@SuppressWarnings("UnusedMethod")
|
||||
private void onPlayerLoadedChunk(ServerPlayer player, MutableObject<ClientboundLevelChunkWithLightPacket> packetCache, LevelChunk chunk, CallbackInfo callback) {
|
||||
CommonHooks.onChunkWatch(chunk, player);
|
||||
}
|
||||
|
||||
@Inject(method = "updateChunkScheduling", at = @At("HEAD"))
|
||||
@SuppressWarnings("UnusedMethod")
|
||||
private void onUpdateChunkScheduling(long chunkPos, int newLevel, @Nullable ChunkHolder holder, int oldLevel, CallbackInfoReturnable<ChunkHolder> callback) {
|
||||
CommonHooks.onChunkTicketLevelChanged(level, chunkPos, oldLevel, newLevel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -96,6 +97,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);
|
||||
|
||||
@@ -22,11 +22,15 @@ import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
|
||||
import dan200.computercraft.shared.util.CapabilityProvider;
|
||||
import dan200.computercraft.shared.util.SidedCapabilityProvider;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.CommandBlockEntity;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraftforge.event.*;
|
||||
import net.minecraftforge.event.entity.EntityJoinLevelEvent;
|
||||
import net.minecraftforge.event.entity.living.LivingDropsEvent;
|
||||
import net.minecraftforge.event.level.ChunkEvent;
|
||||
import net.minecraftforge.event.level.ChunkTicketLevelUpdatedEvent;
|
||||
import net.minecraftforge.event.level.ChunkWatchEvent;
|
||||
import net.minecraftforge.event.server.ServerStartingEvent;
|
||||
import net.minecraftforge.event.server.ServerStoppedEvent;
|
||||
@@ -67,11 +71,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.Watch 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));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user