7c2dc4b342
* finish implementing all adventure components in codecs * add some initial tests * Add round trip tests for text and translatable components * Add more round trip test data (score component is failing) * Add more round trip test data * Fix SCORE_COMPONENT_MAP_CODEC * Improve test failure messages * Add failure cases * Add a couple more test data * Make use of AdventureCodecs * Update patches after rebase * Squash changes into adventure patch * Fix AT formatting * update comment --------- Co-authored-by: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
2114 Zeilen
73 KiB
Diff
2114 Zeilen
73 KiB
Diff
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: Space Walker <spacedoesrs@gmail.com>
|
|
Date: Wed, 8 Jun 2022 18:47:18 +0200
|
|
Subject: [PATCH] Add Alternate Current redstone implementation
|
|
|
|
Author: Space Walker <spacedoesrs@gmail.com>
|
|
|
|
Original license: MIT
|
|
Original project: https://github.com/SpaceWalkerRS/alternate-current
|
|
|
|
This patch adds Alternate Current's redstone implementation as an alternative to vanilla and Eigencraft's.
|
|
Performance of (de)powering redstone dust is many times faster than vanilla, and even exceeds Eigencraft.
|
|
Similar to Eigencraft, Alternate Current heavily changes the update order of redstone dust. This means any contraption that
|
|
is location dependent in vanilla will either work everywhere or nowhere when using Alternate Current/Eigencraft. Beyond that
|
|
parity issues should be rare for both implementations, though Alternate Current has not been tested as thoroughly, so I
|
|
cannot comment on how the two compare in that aspect.
|
|
|
|
Alternate Current needs the following modifications:
|
|
* Level/ServerLevel: Each level has its own 'wire handler' that handles redstone dust power changes.
|
|
* RedStoneWireBlock: Replace calls to vanilla's or Eigencraft's methods for handling power changes with calls to
|
|
Alternate Current's wire handler.
|
|
|
|
diff --git a/src/main/java/alternate/current/wire/LevelHelper.java b/src/main/java/alternate/current/wire/LevelHelper.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..f55c5c67b8461e9ef5614ea1a37f6e2866f39be3
|
|
--- /dev/null
|
|
+++ b/src/main/java/alternate/current/wire/LevelHelper.java
|
|
@@ -0,0 +1,65 @@
|
|
+package alternate.current.wire;
|
|
+
|
|
+import org.bukkit.event.block.BlockRedstoneEvent;
|
|
+
|
|
+import net.minecraft.core.BlockPos;
|
|
+import net.minecraft.server.level.ServerLevel;
|
|
+import net.minecraft.world.level.block.Block;
|
|
+import net.minecraft.world.level.block.state.BlockState;
|
|
+import net.minecraft.world.level.chunk.ChunkAccess;
|
|
+import net.minecraft.world.level.chunk.ChunkStatus;
|
|
+import net.minecraft.world.level.chunk.LevelChunkSection;
|
|
+
|
|
+public class LevelHelper {
|
|
+
|
|
+ static int doRedstoneEvent(ServerLevel level, BlockPos pos, int prevPower, int newPower) {
|
|
+ BlockRedstoneEvent event = new BlockRedstoneEvent(level.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()), prevPower, newPower);
|
|
+ level.getCraftServer().getPluginManager().callEvent(event);
|
|
+
|
|
+ return event.getNewCurrent();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * An optimized version of {@link net.minecraft.world.level.Level#setBlock
|
|
+ * Level.setBlock}. Since this method is only used to update redstone wire block
|
|
+ * states, lighting checks, height map updates, and block entity updates are
|
|
+ * omitted.
|
|
+ */
|
|
+ static boolean setWireState(ServerLevel level, BlockPos pos, BlockState state, boolean updateNeighborShapes) {
|
|
+ int y = pos.getY();
|
|
+
|
|
+ if (y < level.getMinBuildHeight() || y >= level.getMaxBuildHeight()) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ int x = pos.getX();
|
|
+ int z = pos.getZ();
|
|
+ int index = level.getSectionIndex(y);
|
|
+
|
|
+ ChunkAccess chunk = level.getChunk(x >> 4, z >> 4, ChunkStatus.FULL, true);
|
|
+ LevelChunkSection section = chunk.getSections()[index];
|
|
+
|
|
+ if (section == null) {
|
|
+ return false; // we should never get here
|
|
+ }
|
|
+
|
|
+ BlockState prevState = section.setBlockState(x & 15, y & 15, z & 15, state);
|
|
+
|
|
+ if (state == prevState) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ // notify clients of the BlockState change
|
|
+ level.getChunkSource().blockChanged(pos);
|
|
+ // mark the chunk for saving
|
|
+ chunk.setUnsaved(true);
|
|
+
|
|
+ if (updateNeighborShapes) {
|
|
+ prevState.updateIndirectNeighbourShapes(level, pos, Block.UPDATE_CLIENTS);
|
|
+ state.updateNeighbourShapes(level, pos, Block.UPDATE_CLIENTS);
|
|
+ state.updateIndirectNeighbourShapes(level, pos, Block.UPDATE_CLIENTS);
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/alternate/current/wire/Node.java b/src/main/java/alternate/current/wire/Node.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..8af6c69098e64945361d116b5fd6ac21e97fcd8d
|
|
--- /dev/null
|
|
+++ b/src/main/java/alternate/current/wire/Node.java
|
|
@@ -0,0 +1,113 @@
|
|
+package alternate.current.wire;
|
|
+
|
|
+import java.util.Arrays;
|
|
+
|
|
+import alternate.current.wire.WireHandler.Directions;
|
|
+
|
|
+import net.minecraft.core.BlockPos;
|
|
+import net.minecraft.server.level.ServerLevel;
|
|
+import net.minecraft.world.level.block.Blocks;
|
|
+import net.minecraft.world.level.block.state.BlockState;
|
|
+
|
|
+/**
|
|
+ * A Node represents a block in the world. It also holds a few other pieces of
|
|
+ * information that speed up the calculations in the WireHandler class.
|
|
+ *
|
|
+ * @author Space Walker
|
|
+ */
|
|
+public class Node {
|
|
+
|
|
+ // flags that encode the Node type
|
|
+ private static final int CONDUCTOR = 0b01;
|
|
+ private static final int SOURCE = 0b10;
|
|
+
|
|
+ final ServerLevel level;
|
|
+ final Node[] neighbors;
|
|
+
|
|
+ BlockPos pos;
|
|
+ BlockState state;
|
|
+ boolean invalid;
|
|
+
|
|
+ private int flags;
|
|
+
|
|
+ /** The previous node in the priority queue. */
|
|
+ Node prev_node;
|
|
+ /** The next node in the priority queue. */
|
|
+ Node next_node;
|
|
+ /** The priority with which this node was queued. */
|
|
+ int priority;
|
|
+ /** The wire that queued this node for an update. */
|
|
+ WireNode neighborWire;
|
|
+
|
|
+ Node(ServerLevel level) {
|
|
+ this.level = level;
|
|
+ this.neighbors = new Node[Directions.ALL.length];
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean equals(Object obj) {
|
|
+ if (this == obj) {
|
|
+ return true;
|
|
+ }
|
|
+ if (!(obj instanceof Node)) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ Node node = (Node)obj;
|
|
+
|
|
+ return level == node.level && pos.equals(node.pos);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public int hashCode() {
|
|
+ return pos.hashCode();
|
|
+ }
|
|
+
|
|
+ Node set(BlockPos pos, BlockState state, boolean clearNeighbors) {
|
|
+ if (state.is(Blocks.REDSTONE_WIRE)) {
|
|
+ throw new IllegalStateException("Cannot update a regular Node to a WireNode!");
|
|
+ }
|
|
+
|
|
+ if (clearNeighbors) {
|
|
+ Arrays.fill(neighbors, null);
|
|
+ }
|
|
+
|
|
+ this.pos = pos.immutable();
|
|
+ this.state = state;
|
|
+ this.invalid = false;
|
|
+
|
|
+ this.flags = 0;
|
|
+
|
|
+ if (this.state.isRedstoneConductor(this.level, this.pos)) {
|
|
+ this.flags |= CONDUCTOR;
|
|
+ }
|
|
+ if (this.state.isSignalSource()) {
|
|
+ this.flags |= SOURCE;
|
|
+ }
|
|
+
|
|
+ return this;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Determine the priority with which this node should be queued.
|
|
+ */
|
|
+ int priority() {
|
|
+ return neighborWire.priority;
|
|
+ }
|
|
+
|
|
+ public boolean isWire() {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ public boolean isConductor() {
|
|
+ return (flags & CONDUCTOR) != 0;
|
|
+ }
|
|
+
|
|
+ public boolean isSignalSource() {
|
|
+ return (flags & SOURCE) != 0;
|
|
+ }
|
|
+
|
|
+ public WireNode asWire() {
|
|
+ throw new UnsupportedOperationException("Not a WireNode!");
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/alternate/current/wire/PriorityQueue.java b/src/main/java/alternate/current/wire/PriorityQueue.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..d71b4d0e4c44a2620b41b89475412db53bea20ed
|
|
--- /dev/null
|
|
+++ b/src/main/java/alternate/current/wire/PriorityQueue.java
|
|
@@ -0,0 +1,211 @@
|
|
+package alternate.current.wire;
|
|
+
|
|
+import java.util.AbstractQueue;
|
|
+import java.util.Arrays;
|
|
+import java.util.Iterator;
|
|
+
|
|
+import net.minecraft.world.level.redstone.Redstone;
|
|
+
|
|
+public class PriorityQueue extends AbstractQueue<Node> {
|
|
+
|
|
+ private static final int OFFSET = -Redstone.SIGNAL_MIN;
|
|
+
|
|
+ /** The last node for each priority value. */
|
|
+ private final Node[] tails;
|
|
+
|
|
+ private Node head;
|
|
+ private Node tail;
|
|
+
|
|
+ private int size;
|
|
+
|
|
+ PriorityQueue() {
|
|
+ this.tails = new Node[(Redstone.SIGNAL_MAX + OFFSET) + 1];
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean offer(Node node) {
|
|
+ if (node == null) {
|
|
+ throw new NullPointerException();
|
|
+ }
|
|
+
|
|
+ int priority = node.priority();
|
|
+
|
|
+ if (contains(node)) {
|
|
+ if (node.priority == priority) {
|
|
+ // already queued with this priority; exit
|
|
+ return false;
|
|
+ } else {
|
|
+ // already queued with different priority; move it
|
|
+ move(node, priority);
|
|
+ }
|
|
+ } else {
|
|
+ insert(node, priority);
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Node poll() {
|
|
+ if (head == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ Node node = head;
|
|
+ Node next = node.next_node;
|
|
+
|
|
+ if (next == null) {
|
|
+ clear(); // reset the tails array
|
|
+ } else {
|
|
+ if (node.priority != next.priority) {
|
|
+ // If the head is also a tail, its entry in the array
|
|
+ // can be cleared; there is no previous node with the
|
|
+ // same priority to take its place.
|
|
+ tails[node.priority + OFFSET] = null;
|
|
+ }
|
|
+
|
|
+ node.next_node = null;
|
|
+ next.prev_node = null;
|
|
+ head = next;
|
|
+
|
|
+ size--;
|
|
+ }
|
|
+
|
|
+ return node;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Node peek() {
|
|
+ return head;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void clear() {
|
|
+ for (Node node = head; node != null; ) {
|
|
+ Node n = node;
|
|
+ node = node.next_node;
|
|
+
|
|
+ n.prev_node = null;
|
|
+ n.next_node = null;
|
|
+ }
|
|
+
|
|
+ Arrays.fill(tails, null);
|
|
+
|
|
+ head = null;
|
|
+ tail = null;
|
|
+
|
|
+ size = 0;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Iterator<Node> iterator() {
|
|
+ throw new UnsupportedOperationException();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public int size() {
|
|
+ return size;
|
|
+ }
|
|
+
|
|
+ public boolean contains(Node node) {
|
|
+ return node == head || node.prev_node != null;
|
|
+ }
|
|
+
|
|
+ private void move(Node node, int priority) {
|
|
+ remove(node);
|
|
+ insert(node, priority);
|
|
+ }
|
|
+
|
|
+ private void remove(Node node) {
|
|
+ Node prev = node.prev_node;
|
|
+ Node next = node.next_node;
|
|
+
|
|
+ if (node == tail || node.priority != next.priority) {
|
|
+ // assign a new tail for this node's priority
|
|
+ if (node == head || node.priority != prev.priority) {
|
|
+ // there is no other node with the same priority; clear
|
|
+ tails[node.priority + OFFSET] = null;
|
|
+ } else {
|
|
+ // the previous node in the queue becomes the tail
|
|
+ tails[node.priority + OFFSET] = prev;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (node == head) {
|
|
+ head = next;
|
|
+ } else {
|
|
+ prev.next_node = next;
|
|
+ }
|
|
+ if (node == tail) {
|
|
+ tail = prev;
|
|
+ } else {
|
|
+ next.prev_node = prev;
|
|
+ }
|
|
+
|
|
+ node.prev_node = null;
|
|
+ node.next_node = null;
|
|
+
|
|
+ size--;
|
|
+ }
|
|
+
|
|
+ private void insert(Node node, int priority) {
|
|
+ node.priority = priority;
|
|
+
|
|
+ // nodes are sorted by priority (highest to lowest)
|
|
+ // nodes with the same priority are ordered FIFO
|
|
+ if (head == null) {
|
|
+ // first element in this queue \o/
|
|
+ head = tail = node;
|
|
+ } else if (priority > head.priority) {
|
|
+ linkHead(node);
|
|
+ } else if (priority <= tail.priority) {
|
|
+ linkTail(node);
|
|
+ } else {
|
|
+ // since the node is neither the head nor the tail
|
|
+ // findPrev is guaranteed to find a non-null element
|
|
+ linkAfter(findPrev(node), node);
|
|
+ }
|
|
+
|
|
+ tails[priority + OFFSET] = node;
|
|
+
|
|
+ size++;
|
|
+ }
|
|
+
|
|
+ private void linkHead(Node node) {
|
|
+ node.next_node = head;
|
|
+ head.prev_node = node;
|
|
+ head = node;
|
|
+ }
|
|
+
|
|
+ private void linkTail(Node node) {
|
|
+ tail.next_node = node;
|
|
+ node.prev_node = tail;
|
|
+ tail = node;
|
|
+ }
|
|
+
|
|
+ private void linkAfter(Node prev, Node node) {
|
|
+ linkBetween(prev, node, prev.next_node);
|
|
+ }
|
|
+
|
|
+ private void linkBetween(Node prev, Node node, Node next) {
|
|
+ prev.next_node = node;
|
|
+ node.prev_node = prev;
|
|
+
|
|
+ node.next_node = next;
|
|
+ next.prev_node = node;
|
|
+ }
|
|
+
|
|
+ private Node findPrev(Node node) {
|
|
+ Node prev = null;
|
|
+
|
|
+ for (int i = node.priority + OFFSET; i < tails.length; i++) {
|
|
+ prev = tails[i];
|
|
+
|
|
+ if (prev != null) {
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return prev;
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/alternate/current/wire/SimpleQueue.java b/src/main/java/alternate/current/wire/SimpleQueue.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..2b30074252551e1dc55d5be17d26fb4a2d8eb2e4
|
|
--- /dev/null
|
|
+++ b/src/main/java/alternate/current/wire/SimpleQueue.java
|
|
@@ -0,0 +1,112 @@
|
|
+package alternate.current.wire;
|
|
+
|
|
+import java.util.AbstractQueue;
|
|
+import java.util.Iterator;
|
|
+
|
|
+public class SimpleQueue extends AbstractQueue<WireNode> {
|
|
+
|
|
+ private WireNode head;
|
|
+ private WireNode tail;
|
|
+
|
|
+ private int size;
|
|
+
|
|
+ SimpleQueue() {
|
|
+
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean offer(WireNode node) {
|
|
+ if (node == null) {
|
|
+ throw new NullPointerException();
|
|
+ }
|
|
+
|
|
+ if (tail == null) {
|
|
+ head = tail = node;
|
|
+ } else {
|
|
+ tail.next_wire = node;
|
|
+ tail = node;
|
|
+ }
|
|
+
|
|
+ size++;
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public WireNode poll() {
|
|
+ if (head == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ WireNode node = head;
|
|
+ WireNode next = node.next_wire;
|
|
+
|
|
+ if (next == null) {
|
|
+ head = tail = null;
|
|
+ } else {
|
|
+ node.next_wire = null;
|
|
+ head = next;
|
|
+ }
|
|
+
|
|
+ size--;
|
|
+
|
|
+ return node;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public WireNode peek() {
|
|
+ return head;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void clear() {
|
|
+ for (WireNode node = head; node != null; ) {
|
|
+ WireNode n = node;
|
|
+ node = node.next_wire;
|
|
+
|
|
+ n.next_wire = null;
|
|
+ }
|
|
+
|
|
+ head = null;
|
|
+ tail = null;
|
|
+
|
|
+ size = 0;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Iterator<WireNode> iterator() {
|
|
+ return new SimpleIterator();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public int size() {
|
|
+ return size;
|
|
+ }
|
|
+
|
|
+ private class SimpleIterator implements Iterator<WireNode> {
|
|
+
|
|
+ private WireNode curr;
|
|
+ private WireNode next;
|
|
+
|
|
+ private SimpleIterator() {
|
|
+ next = head;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean hasNext() {
|
|
+ if (next == null && curr != null) {
|
|
+ next = curr.next_wire;
|
|
+ }
|
|
+
|
|
+ return next != null;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public WireNode next() {
|
|
+ curr = next;
|
|
+ next = curr.next_wire;
|
|
+
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/alternate/current/wire/WireConnection.java b/src/main/java/alternate/current/wire/WireConnection.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..4fd8cb29024330397cfe4cbc1f237d285bfb7b3e
|
|
--- /dev/null
|
|
+++ b/src/main/java/alternate/current/wire/WireConnection.java
|
|
@@ -0,0 +1,30 @@
|
|
+package alternate.current.wire;
|
|
+
|
|
+/**
|
|
+ * This class represents a connection between some WireNode (the 'owner') and a
|
|
+ * neighboring WireNode. Two wires are considered to be connected if power can
|
|
+ * flow from one wire to the other (and/or vice versa).
|
|
+ *
|
|
+ * @author Space Walker
|
|
+ */
|
|
+public class WireConnection {
|
|
+
|
|
+ /** The connected wire. */
|
|
+ final WireNode wire;
|
|
+ /** Cardinal direction to the connected wire. */
|
|
+ final int iDir;
|
|
+ /** True if the owner of the connection can provide power to the connected wire. */
|
|
+ final boolean offer;
|
|
+ /** True if the connected wire can provide power to the owner of the connection. */
|
|
+ final boolean accept;
|
|
+
|
|
+ /** The next connection in the sequence. */
|
|
+ WireConnection next;
|
|
+
|
|
+ WireConnection(WireNode wire, int iDir, boolean offer, boolean accept) {
|
|
+ this.wire = wire;
|
|
+ this.iDir = iDir;
|
|
+ this.offer = offer;
|
|
+ this.accept = accept;
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/alternate/current/wire/WireConnectionManager.java b/src/main/java/alternate/current/wire/WireConnectionManager.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..5a7209f05b549c222f6c9bc2af2a35790964947e
|
|
--- /dev/null
|
|
+++ b/src/main/java/alternate/current/wire/WireConnectionManager.java
|
|
@@ -0,0 +1,136 @@
|
|
+package alternate.current.wire;
|
|
+
|
|
+import java.util.Arrays;
|
|
+import java.util.function.Consumer;
|
|
+
|
|
+import alternate.current.wire.WireHandler.Directions;
|
|
+import alternate.current.wire.WireHandler.NodeProvider;
|
|
+
|
|
+public class WireConnectionManager {
|
|
+
|
|
+ /** The owner of these connections. */
|
|
+ final WireNode owner;
|
|
+
|
|
+ /** The first connection for each cardinal direction. */
|
|
+ private final WireConnection[] heads;
|
|
+
|
|
+ private WireConnection head;
|
|
+ private WireConnection tail;
|
|
+
|
|
+ /** The total number of connections. */
|
|
+ int total;
|
|
+
|
|
+ /**
|
|
+ * A 4 bit number that encodes in which direction(s) the owner has connections
|
|
+ * to other wires.
|
|
+ */
|
|
+ private int flowTotal;
|
|
+ /** The direction of flow based connections to other wires. */
|
|
+ int iFlowDir;
|
|
+
|
|
+ WireConnectionManager(WireNode owner) {
|
|
+ this.owner = owner;
|
|
+
|
|
+ this.heads = new WireConnection[Directions.HORIZONTAL.length];
|
|
+
|
|
+ this.total = 0;
|
|
+
|
|
+ this.flowTotal = 0;
|
|
+ this.iFlowDir = -1;
|
|
+ }
|
|
+
|
|
+ void set(NodeProvider nodes) {
|
|
+ if (total > 0) {
|
|
+ clear();
|
|
+ }
|
|
+
|
|
+ boolean belowIsConductor = nodes.getNeighbor(owner, Directions.DOWN).isConductor();
|
|
+ boolean aboveIsConductor = nodes.getNeighbor(owner, Directions.UP).isConductor();
|
|
+
|
|
+ for (int iDir = 0; iDir < Directions.HORIZONTAL.length; iDir++) {
|
|
+ Node neighbor = nodes.getNeighbor(owner, iDir);
|
|
+
|
|
+ if (neighbor.isWire()) {
|
|
+ add(neighbor.asWire(), iDir, true, true);
|
|
+
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ boolean sideIsConductor = neighbor.isConductor();
|
|
+
|
|
+ if (!sideIsConductor) {
|
|
+ Node node = nodes.getNeighbor(neighbor, Directions.DOWN);
|
|
+
|
|
+ if (node.isWire()) {
|
|
+ add(node.asWire(), iDir, belowIsConductor, true);
|
|
+ }
|
|
+ }
|
|
+ if (!aboveIsConductor) {
|
|
+ Node node = nodes.getNeighbor(neighbor, Directions.UP);
|
|
+
|
|
+ if (node.isWire()) {
|
|
+ add(node.asWire(), iDir, true, sideIsConductor);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (total > 0) {
|
|
+ iFlowDir = WireHandler.FLOW_IN_TO_FLOW_OUT[flowTotal];
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void clear() {
|
|
+ Arrays.fill(heads, null);
|
|
+
|
|
+ head = null;
|
|
+ tail = null;
|
|
+
|
|
+ total = 0;
|
|
+
|
|
+ flowTotal = 0;
|
|
+ iFlowDir = -1;
|
|
+ }
|
|
+
|
|
+ private void add(WireNode wire, int iDir, boolean offer, boolean accept) {
|
|
+ add(new WireConnection(wire, iDir, offer, accept));
|
|
+ }
|
|
+
|
|
+ private void add(WireConnection connection) {
|
|
+ if (head == null) {
|
|
+ head = connection;
|
|
+ tail = connection;
|
|
+ } else {
|
|
+ tail.next = connection;
|
|
+ tail = connection;
|
|
+ }
|
|
+
|
|
+ total++;
|
|
+
|
|
+ if (heads[connection.iDir] == null) {
|
|
+ heads[connection.iDir] = connection;
|
|
+ flowTotal |= (1 << connection.iDir);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Iterate over all connections. Use this method if the iteration order is not
|
|
+ * important.
|
|
+ */
|
|
+ void forEach(Consumer<WireConnection> consumer) {
|
|
+ for (WireConnection c = head; c != null; c = c.next) {
|
|
+ consumer.accept(c);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Iterate over all connections. Use this method if the iteration order is
|
|
+ * important.
|
|
+ */
|
|
+ void forEach(Consumer<WireConnection> consumer, int iFlowDir) {
|
|
+ for (int iDir : WireHandler.CARDINAL_UPDATE_ORDERS[iFlowDir]) {
|
|
+ for (WireConnection c = heads[iDir]; c != null && c.iDir == iDir; c = c.next) {
|
|
+ consumer.accept(c);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/alternate/current/wire/WireHandler.java b/src/main/java/alternate/current/wire/WireHandler.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..02d0a3c3edd36b636163c5c912b26336fe87d16c
|
|
--- /dev/null
|
|
+++ b/src/main/java/alternate/current/wire/WireHandler.java
|
|
@@ -0,0 +1,1150 @@
|
|
+package alternate.current.wire;
|
|
+
|
|
+import java.util.Iterator;
|
|
+import java.util.Queue;
|
|
+import java.util.function.Consumer;
|
|
+
|
|
+import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ObjectMaps;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
|
+
|
|
+import net.minecraft.core.BlockPos;
|
|
+import net.minecraft.core.Direction;
|
|
+import net.minecraft.server.level.ServerLevel;
|
|
+import net.minecraft.world.level.block.Block;
|
|
+import net.minecraft.world.level.block.Blocks;
|
|
+import net.minecraft.world.level.block.state.BlockState;
|
|
+import net.minecraft.world.level.redstone.Redstone;
|
|
+
|
|
+/**
|
|
+ * This class handles power changes for redstone wire. The algorithm was
|
|
+ * designed with the following goals in mind:
|
|
+ * <br>
|
|
+ * 1. Minimize the number of times a wire checks its surroundings to determine
|
|
+ * its power level.
|
|
+ * <br>
|
|
+ * 2. Minimize the number of block and shape updates emitted.
|
|
+ * <br>
|
|
+ * 3. Emit block and shape updates in a deterministic, non-locational order,
|
|
+ * fixing bug MC-11193.
|
|
+ *
|
|
+ * <p>
|
|
+ * In Vanilla redstone wire is laggy because it fails on points 1 and 2.
|
|
+ *
|
|
+ * <p>
|
|
+ * Redstone wire updates recursively and each wire calculates its power level in
|
|
+ * isolation rather than in the context of the network it is a part of. This
|
|
+ * means a wire in a grid can change its power level over half a dozen times
|
|
+ * before settling on its final value. This problem used to be worse in 1.13 and
|
|
+ * below, where a wire would only decrease its power level by 1 at a time.
|
|
+ *
|
|
+ * <p>
|
|
+ * In addition to this, a wire emits 42 block updates and up to 22 shape updates
|
|
+ * each time it changes its power level.
|
|
+ *
|
|
+ * <p>
|
|
+ * Of those 42 block updates, 6 are to itself, which are thus not only
|
|
+ * redundant, but a big source of lag, since those cause the wire to
|
|
+ * unnecessarily re-calculate its power level. A block only has 24 neighbors
|
|
+ * within a Manhattan distance of 2, meaning 12 of the remaining 36 block
|
|
+ * updates are duplicates and thus also redundant.
|
|
+ *
|
|
+ * <p>
|
|
+ * Of the 22 shape updates, only 6 are strictly necessary. The other 16 are sent
|
|
+ * to blocks diagonally above and below. These are necessary if a wire changes
|
|
+ * its connections, but not when it changes its power level.
|
|
+ *
|
|
+ * <p>
|
|
+ * Redstone wire in Vanilla also fails on point 3, though this is more of a
|
|
+ * quality-of-life issue than a lag issue. The recursive nature in which it
|
|
+ * updates, combined with the location-dependent order in which each wire
|
|
+ * updates its neighbors, makes the order in which neighbors of a wire network
|
|
+ * are updated incredibly inconsistent and seemingly random.
|
|
+ *
|
|
+ * <p>
|
|
+ * Alternate Current fixes each of these problems as follows.
|
|
+ *
|
|
+ * <p>
|
|
+ * 1. To make sure a wire calculates its power level as little as possible, we
|
|
+ * remove the recursive nature in which redstone wire updates in Vanilla.
|
|
+ * Instead, we build a network of connected wires, find those wires that receive
|
|
+ * redstone power from "outside" the network, and spread the power from there.
|
|
+ * This has a few advantages:
|
|
+ * <br>
|
|
+ * - Each wire checks for power from non-wire components at most once, and from
|
|
+ * nearby wires just twice.
|
|
+ * <br>
|
|
+ * - Each wire only sets its power level in the world once. This is important,
|
|
+ * because calls to Level.setBlock are even more expensive than calls to
|
|
+ * Level.getBlockState.
|
|
+ *
|
|
+ * <p>
|
|
+ * 2. There are 2 obvious ways in which we can reduce the number of block and
|
|
+ * shape updates.
|
|
+ * <br>
|
|
+ * - Get rid of the 18 redundant block updates and 16 redundant shape updates,
|
|
+ * so each wire only emits 24 block updates and 6 shape updates whenever it
|
|
+ * changes its power level.
|
|
+ * <br>
|
|
+ * - Only emit block updates and shape updates once a wire reaches its final
|
|
+ * power level, rather than at each intermediary stage.
|
|
+ * <br>
|
|
+ * For an individual wire, these two optimizations are the best you can do, but
|
|
+ * for an entire grid, you can do better!
|
|
+ *
|
|
+ * <p>
|
|
+ * Since we calculate the power of the entire network, sending block and shape
|
|
+ * updates to the wires in it is redundant. Removing those updates can reduce
|
|
+ * the number of block and shape updates by up to 20%.
|
|
+ *
|
|
+ * <p>
|
|
+ * 3. To make the order of block updates to neighbors of a network
|
|
+ * deterministic, the first thing we must do is to replace the location-
|
|
+ * dependent order in which a wire updates its neighbors. Instead, we base it on
|
|
+ * the direction of power flow. This part of the algorithm was heavily inspired
|
|
+ * by theosib's 'RedstoneWireTurbo', which you can read more about in theosib's
|
|
+ * comment on Mojira <a href="https://bugs.mojang.com/browse/MC-81098?focusedCommentId=420777&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-420777">here</a>
|
|
+ * or by checking out its implementation in carpet mod <a href="https://github.com/gnembon/fabric-carpet/blob/master/src/main/java/carpet/helpers/RedstoneWireTurbo.java">here</a>.
|
|
+ *
|
|
+ * <p>
|
|
+ * The idea is to determine the direction of power flow through a wire based on
|
|
+ * the power it receives from neighboring wires. For example, if the only power
|
|
+ * a wire receives is from a neighboring wire to its west, it can be said that
|
|
+ * the direction of power flow through the wire is east.
|
|
+ *
|
|
+ * <p>
|
|
+ * We make the order of block updates to neighbors of a wire depend on what is
|
|
+ * determined to be the direction of power flow. This not only removes
|
|
+ * locationality entirely, it even removes directionality in a large number of
|
|
+ * cases. Unlike in 'RedstoneWireTurbo', however, I have decided to keep a
|
|
+ * directional element in ambiguous cases, rather than to introduce randomness,
|
|
+ * though this is trivial to change.
|
|
+ *
|
|
+ * <p>
|
|
+ * While this change fixes the block update order of individual wires, we must
|
|
+ * still address the overall block update order of a network. This turns out to
|
|
+ * be a simple fix, because of a change we made earlier: we search through the
|
|
+ * network for wires that receive power from outside it, and spread the power
|
|
+ * from there. If we make each wire transmit its power to neighboring wires in
|
|
+ * an order dependent on the direction of power flow, we end up with a
|
|
+ * non-locational and largely non-directional wire update order.
|
|
+ *
|
|
+ * @author Space Walker
|
|
+ */
|
|
+public class WireHandler {
|
|
+
|
|
+ public static class Directions {
|
|
+
|
|
+ public static final Direction[] ALL = { Direction.WEST, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.DOWN, Direction.UP };
|
|
+ public static final Direction[] HORIZONTAL = { Direction.WEST, Direction.NORTH, Direction.EAST, Direction.SOUTH };
|
|
+
|
|
+ // Indices for the arrays above.
|
|
+ // The cardinal directions are ordered clockwise. This allows
|
|
+ // for conversion between relative and absolute directions
|
|
+ // ('left' 'right' vs 'east' 'west') with simple arithmetic:
|
|
+ // If some Direction index 'iDir' is considered 'forward', then
|
|
+ // '(iDir + 1) & 0b11' is 'right', '(iDir + 2) & 0b11' is 'backward', etc.
|
|
+ public static final int WEST = 0b000; // 0
|
|
+ public static final int NORTH = 0b001; // 1
|
|
+ public static final int EAST = 0b010; // 2
|
|
+ public static final int SOUTH = 0b011; // 3
|
|
+ public static final int DOWN = 0b100; // 4
|
|
+ public static final int UP = 0b101; // 5
|
|
+
|
|
+ public static int iOpposite(int iDir) {
|
|
+ return iDir ^ (0b10 >>> (iDir >>> 2));
|
|
+ }
|
|
+
|
|
+ // Each array is placed at the index that encodes the direction that is missing
|
|
+ // from the array.
|
|
+ private static final int[][] I_EXCEPT = {
|
|
+ { NORTH, EAST, SOUTH, DOWN, UP },
|
|
+ { WEST, EAST, SOUTH, DOWN, UP },
|
|
+ { WEST, NORTH, SOUTH, DOWN, UP },
|
|
+ { WEST, NORTH, EAST, DOWN, UP },
|
|
+ { WEST, NORTH, EAST, SOUTH, UP },
|
|
+ { WEST, NORTH, EAST, SOUTH, DOWN }
|
|
+ };
|
|
+ private static final int[][] I_EXCEPT_CARDINAL = {
|
|
+ { NORTH, EAST, SOUTH },
|
|
+ { WEST, EAST, SOUTH },
|
|
+ { WEST, NORTH, SOUTH },
|
|
+ { WEST, NORTH, EAST, },
|
|
+ { WEST, NORTH, EAST, SOUTH },
|
|
+ { WEST, NORTH, EAST, SOUTH }
|
|
+ };
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * This conversion table takes in information about incoming flow, and outputs
|
|
+ * the determined outgoing flow.
|
|
+ *
|
|
+ * <p>
|
|
+ * The input is a 4 bit number that encodes the incoming flow. Each bit
|
|
+ * represents a cardinal direction, and when it is 'on', there is flow in that
|
|
+ * direction.
|
|
+ *
|
|
+ * <p>
|
|
+ * The output is a single Direction index, or -1 for ambiguous cases.
|
|
+ *
|
|
+ * <p>
|
|
+ * The outgoing flow is determined as follows:
|
|
+ *
|
|
+ * <p>
|
|
+ * If there is just 1 direction of incoming flow, that direction will be the
|
|
+ * direction of outgoing flow.
|
|
+ *
|
|
+ * <p>
|
|
+ * If there are 2 directions of incoming flow, and these directions are not each
|
|
+ * other's opposites, the direction that is 'more clockwise' will be the
|
|
+ * direction of outgoing flow. More precisely, the direction that is 1 clockwise
|
|
+ * turn from the other is picked.
|
|
+ *
|
|
+ * <p>
|
|
+ * If there are 3 directions of incoming flow, the two opposing directions
|
|
+ * cancel each other out, and the remaining direction will be the direction of
|
|
+ * outgoing flow.
|
|
+ *
|
|
+ * <p>
|
|
+ * In all other cases, the flow is completely ambiguous.
|
|
+ */
|
|
+ static final int[] FLOW_IN_TO_FLOW_OUT = {
|
|
+ -1, // 0b0000: - -> x
|
|
+ Directions.WEST, // 0b0001: west -> west
|
|
+ Directions.NORTH, // 0b0010: north -> north
|
|
+ Directions.NORTH, // 0b0011: west/north -> north
|
|
+ Directions.EAST, // 0b0100: east -> east
|
|
+ -1, // 0b0101: west/east -> x
|
|
+ Directions.EAST, // 0b0110: north/east -> east
|
|
+ Directions.NORTH, // 0b0111: west/north/east -> north
|
|
+ Directions.SOUTH, // 0b1000: south -> south
|
|
+ Directions.WEST, // 0b1001: west/south -> west
|
|
+ -1, // 0b1010: north/south -> x
|
|
+ Directions.WEST, // 0b1011: west/north/south -> west
|
|
+ Directions.SOUTH, // 0b1100: east/south -> south
|
|
+ Directions.SOUTH, // 0b1101: west/east/south -> south
|
|
+ Directions.EAST, // 0b1110: north/east/south -> east
|
|
+ -1, // 0b1111: west/north/east/south -> x
|
|
+ };
|
|
+ /**
|
|
+ * Update orders of all directions. Given that the index encodes the direction
|
|
+ * that is to be considered 'forward', the resulting update order is
|
|
+ * { front, back, right, left, down, up }.
|
|
+ */
|
|
+ static final int[][] FULL_UPDATE_ORDERS = {
|
|
+ { Directions.WEST , Directions.EAST , Directions.NORTH, Directions.SOUTH, Directions.DOWN, Directions.UP },
|
|
+ { Directions.NORTH, Directions.SOUTH, Directions.EAST , Directions.WEST , Directions.DOWN, Directions.UP },
|
|
+ { Directions.EAST , Directions.WEST , Directions.SOUTH, Directions.NORTH, Directions.DOWN, Directions.UP },
|
|
+ { Directions.SOUTH, Directions.NORTH, Directions.WEST , Directions.EAST , Directions.DOWN, Directions.UP }
|
|
+ };
|
|
+ /**
|
|
+ * The default update order of all directions. It is equivalent to the order of
|
|
+ * shape updates in vanilla Minecraft.
|
|
+ */
|
|
+ static final int[] DEFAULT_FULL_UPDATE_ORDER = FULL_UPDATE_ORDERS[0];
|
|
+ /**
|
|
+ * Update orders of cardinal directions. Given that the index encodes the
|
|
+ * direction that is to be considered 'forward', the resulting update order is
|
|
+ * { front, back, right, left }.
|
|
+ */
|
|
+ static final int[][] CARDINAL_UPDATE_ORDERS = {
|
|
+ { Directions.WEST , Directions.EAST , Directions.NORTH, Directions.SOUTH },
|
|
+ { Directions.NORTH, Directions.SOUTH, Directions.EAST , Directions.WEST },
|
|
+ { Directions.EAST , Directions.WEST , Directions.SOUTH, Directions.NORTH },
|
|
+ { Directions.SOUTH, Directions.NORTH, Directions.WEST , Directions.EAST }
|
|
+ };
|
|
+ /**
|
|
+ * The default update order of all cardinal directions.
|
|
+ */
|
|
+ static final int[] DEFAULT_CARDINAL_UPDATE_ORDER = CARDINAL_UPDATE_ORDERS[0];
|
|
+
|
|
+ private static final int POWER_MIN = Redstone.SIGNAL_MIN;
|
|
+ private static final int POWER_MAX = Redstone.SIGNAL_MAX;
|
|
+ private static final int POWER_STEP = 1;
|
|
+
|
|
+ // If Vanilla will ever multi-thread the ticking of levels, there should
|
|
+ // be only one WireHandler per level, in case redstone updates in multiple
|
|
+ // levels at the same time. There are already mods that add multi-threading
|
|
+ // as well.
|
|
+ private final ServerLevel level;
|
|
+
|
|
+ /** Map of wires and neighboring blocks. */
|
|
+ private final Long2ObjectMap<Node> nodes;
|
|
+ /** Queue for the breadth-first search through the network. */
|
|
+ private final Queue<WireNode> search;
|
|
+ /** Queue of updates to wires and neighboring blocks. */
|
|
+ private final Queue<Node> updates;
|
|
+
|
|
+ // Rather than creating new nodes every time a network is updated we keep
|
|
+ // a cache of nodes that can be re-used.
|
|
+ private Node[] nodeCache;
|
|
+ private int nodeCount;
|
|
+
|
|
+ /** Is this WireHandler currently working through the update queue? */
|
|
+ private boolean updating;
|
|
+
|
|
+ public WireHandler(ServerLevel level) {
|
|
+ this.level = level;
|
|
+
|
|
+ this.nodes = new Long2ObjectOpenHashMap<>();
|
|
+ this.search = new SimpleQueue();
|
|
+ this.updates = new PriorityQueue();
|
|
+
|
|
+ this.nodeCache = new Node[16];
|
|
+ this.fillNodeCache(0, 16);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Retrieve the {@link Node Node} that represents the
|
|
+ * block at the given position in the level.
|
|
+ */
|
|
+ private Node getOrAddNode(BlockPos pos) {
|
|
+ return nodes.compute(pos.asLong(), (key, node) -> {
|
|
+ if (node == null) {
|
|
+ // If there is not yet a node at this position, retrieve and
|
|
+ // update one from the cache.
|
|
+ return getNextNode(pos);
|
|
+ }
|
|
+ if (node.invalid) {
|
|
+ return revalidateNode(node);
|
|
+ }
|
|
+
|
|
+ return node;
|
|
+ });
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Remove and return the {@link Node Node} at the given
|
|
+ * position.
|
|
+ */
|
|
+ private Node removeNode(BlockPos pos) {
|
|
+ return nodes.remove(pos.asLong());
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Return a {@link Node Node} that represents the block
|
|
+ * at the given position.
|
|
+ */
|
|
+ private Node getNextNode(BlockPos pos) {
|
|
+ return getNextNode(pos, level.getBlockState(pos));
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Return a node that represents the given position and block state. If it is a
|
|
+ * wire, then create a new {@link WireNode WireNode}.
|
|
+ * Otherwise, grab the next {@link Node Node} from the
|
|
+ * cache and update it.
|
|
+ */
|
|
+ private Node getNextNode(BlockPos pos, BlockState state) {
|
|
+ return state.is(Blocks.REDSTONE_WIRE) ? new WireNode(level, pos, state) : getNextNode().set(pos, state, true);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Grab the first unused node from the cache. If all of the cache is already in
|
|
+ * use, increase it in size first.
|
|
+ */
|
|
+ private Node getNextNode() {
|
|
+ if (nodeCount == nodeCache.length) {
|
|
+ increaseNodeCache();
|
|
+ }
|
|
+
|
|
+ return nodeCache[nodeCount++];
|
|
+ }
|
|
+
|
|
+ private void increaseNodeCache() {
|
|
+ Node[] oldCache = nodeCache;
|
|
+ nodeCache = new Node[oldCache.length << 1];
|
|
+
|
|
+ for (int index = 0; index < oldCache.length; index++) {
|
|
+ nodeCache[index] = oldCache[index];
|
|
+ }
|
|
+
|
|
+ fillNodeCache(oldCache.length, nodeCache.length);
|
|
+ }
|
|
+
|
|
+ private void fillNodeCache(int start, int end) {
|
|
+ for (int index = start; index < end; index++) {
|
|
+ nodeCache[index] = new Node(level);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Try to revalidate the given node by looking at the block state that is
|
|
+ * occupying its position. If the given node is a wire but the block state is
|
|
+ * not, or vice versa, a new node must be created/grabbed from the cache.
|
|
+ * Otherwise, the node can be quickly revalidated with the new block state.
|
|
+ */
|
|
+ private Node revalidateNode(Node node) {
|
|
+ BlockPos pos = node.pos;
|
|
+ BlockState state = level.getBlockState(pos);
|
|
+
|
|
+ boolean wasWire = node.isWire();
|
|
+ boolean isWire = state.is(Blocks.REDSTONE_WIRE);
|
|
+
|
|
+ if (wasWire != isWire) {
|
|
+ return getNextNode(pos, state);
|
|
+ }
|
|
+
|
|
+ node.invalid = false;
|
|
+
|
|
+ if (isWire) {
|
|
+ // No need to update the block state of this wire - it will grab
|
|
+ // the current block state just before setting power anyway.
|
|
+ WireNode wire = node.asWire();
|
|
+
|
|
+ wire.root = false;
|
|
+ wire.discovered = false;
|
|
+ wire.searched = false;
|
|
+ } else {
|
|
+ node.set(pos, state, false);
|
|
+ }
|
|
+
|
|
+ return node;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Retrieve the neighbor of a node in the given direction and create a link
|
|
+ * between the two nodes if they are not yet linked. This link makes accessing
|
|
+ * neighbors of a node signficantly faster.
|
|
+ */
|
|
+ private Node getNeighbor(Node node, int iDir) {
|
|
+ Node neighbor = node.neighbors[iDir];
|
|
+
|
|
+ if (neighbor == null || neighbor.invalid) {
|
|
+ Direction dir = Directions.ALL[iDir];
|
|
+ BlockPos pos = node.pos.relative(dir);
|
|
+
|
|
+ Node oldNeighbor = neighbor;
|
|
+ neighbor = getOrAddNode(pos);
|
|
+
|
|
+ if (neighbor != oldNeighbor) {
|
|
+ int iOpp = Directions.iOpposite(iDir);
|
|
+
|
|
+ node.neighbors[iDir] = neighbor;
|
|
+ neighbor.neighbors[iOpp] = node;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return neighbor;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Iterate over all neighboring nodes of the given wire. The iteration order is
|
|
+ * designed to be an extension of the default block update order, and is
|
|
+ * determined as follows:
|
|
+ * <br>
|
|
+ * 1. The direction of power flow through the wire is to be considered
|
|
+ * 'forward'. The iteration order depends on the neighbors' relative positions
|
|
+ * to the wire.
|
|
+ * <br>
|
|
+ * 2. Each neighbor is identified by the step(s) you must take, starting at the
|
|
+ * wire, to reach it. Each step is 1 block, thus the position of a neighbor is
|
|
+ * encoded by the direction(s) of the step(s), e.g. (right), (down), (up, left),
|
|
+ * etc.
|
|
+ * <br>
|
|
+ * 3. Neighbors are iterated over in pairs that lie on opposite sides of the
|
|
+ * wire.
|
|
+ * <br>
|
|
+ * 4. Neighbors are iterated over in order of their distance from the wire. This
|
|
+ * means they are iterated over in 3 groups: direct neighbors first, then
|
|
+ * diagonal neighbors, and last are the far neighbors that are 2 blocks directly
|
|
+ * out.
|
|
+ * <br>
|
|
+ * 5. The order within each group is determined using the following basic order:
|
|
+ * { front, back, right, left, down, up }. This order was chosen because it
|
|
+ * converts to the following order of absolute directions when west is said to
|
|
+ * be 'forward': { west, east, north, south, down, up } - this is the order of
|
|
+ * shape updates.
|
|
+ */
|
|
+ private void forEachNeighbor(WireNode wire, Consumer<Node> consumer) {
|
|
+ int forward = wire.iFlowDir;
|
|
+ int rightward = (forward + 1) & 0b11;
|
|
+ int backward = (forward + 2) & 0b11;
|
|
+ int leftward = (forward + 3) & 0b11;
|
|
+ int downward = Directions.DOWN;
|
|
+ int upward = Directions.UP;
|
|
+
|
|
+ Node front = getNeighbor(wire, forward);
|
|
+ Node right = getNeighbor(wire, rightward);
|
|
+ Node back = getNeighbor(wire, backward);
|
|
+ Node left = getNeighbor(wire, leftward);
|
|
+ Node below = getNeighbor(wire, downward);
|
|
+ Node above = getNeighbor(wire, upward);
|
|
+
|
|
+ // direct neighbors (6)
|
|
+ consumer.accept(front);
|
|
+ consumer.accept(back);
|
|
+ consumer.accept(right);
|
|
+ consumer.accept(left);
|
|
+ consumer.accept(below);
|
|
+ consumer.accept(above);
|
|
+
|
|
+ // diagonal neighbors (12)
|
|
+ consumer.accept(getNeighbor(front, rightward));
|
|
+ consumer.accept(getNeighbor(back, leftward));
|
|
+ consumer.accept(getNeighbor(front, leftward));
|
|
+ consumer.accept(getNeighbor(back, rightward));
|
|
+ consumer.accept(getNeighbor(front, downward));
|
|
+ consumer.accept(getNeighbor(back, upward));
|
|
+ consumer.accept(getNeighbor(front, upward));
|
|
+ consumer.accept(getNeighbor(back, downward));
|
|
+ consumer.accept(getNeighbor(right, downward));
|
|
+ consumer.accept(getNeighbor(left, upward));
|
|
+ consumer.accept(getNeighbor(right, upward));
|
|
+ consumer.accept(getNeighbor(left, downward));
|
|
+
|
|
+ // far neighbors (6)
|
|
+ consumer.accept(getNeighbor(front, forward));
|
|
+ consumer.accept(getNeighbor(back, backward));
|
|
+ consumer.accept(getNeighbor(right, rightward));
|
|
+ consumer.accept(getNeighbor(left, leftward));
|
|
+ consumer.accept(getNeighbor(below, downward));
|
|
+ consumer.accept(getNeighbor(above, upward));
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * This method should be called whenever a wire receives a block update.
|
|
+ */
|
|
+ public void onWireUpdated(BlockPos pos) {
|
|
+ invalidate();
|
|
+ findRoots(pos);
|
|
+ tryUpdate();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * This method should be called whenever a wire is placed.
|
|
+ */
|
|
+ public void onWireAdded(BlockPos pos) {
|
|
+ Node node = getOrAddNode(pos);
|
|
+
|
|
+ if (!node.isWire()) {
|
|
+ return; // we should never get here
|
|
+ }
|
|
+
|
|
+ WireNode wire = node.asWire();
|
|
+ wire.added = true;
|
|
+
|
|
+ invalidate();
|
|
+ revalidateNode(wire);
|
|
+ findRoot(wire);
|
|
+ tryUpdate();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * This method should be called whenever a wire is removed.
|
|
+ */
|
|
+ public void onWireRemoved(BlockPos pos, BlockState state) {
|
|
+ Node node = removeNode(pos);
|
|
+ WireNode wire;
|
|
+
|
|
+ if (node == null || !node.isWire()) {
|
|
+ wire = new WireNode(level, pos, state);
|
|
+ } else {
|
|
+ wire = node.asWire();
|
|
+ }
|
|
+
|
|
+ wire.invalid = true;
|
|
+ wire.removed = true;
|
|
+
|
|
+ // If these fields are set to 'true', the removal of this wire was part of
|
|
+ // already ongoing power changes, so we can exit early here.
|
|
+ if (updating && wire.shouldBreak) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ invalidate();
|
|
+ revalidateNode(wire);
|
|
+ findRoot(wire);
|
|
+ tryUpdate();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * The nodes map is a snapshot of the state of the world. It becomes invalid
|
|
+ * when power changes are carried out, since the block and shape updates can
|
|
+ * lead to block changes. If these block changes cause the network to be updated
|
|
+ * again every node must be invalidated, and revalidated before it is used
|
|
+ * again. This ensures the power calculations of the network are accurate.
|
|
+ */
|
|
+ private void invalidate() {
|
|
+ if (updating && !nodes.isEmpty()) {
|
|
+ Iterator<Entry<Node>> it = Long2ObjectMaps.fastIterator(nodes);
|
|
+
|
|
+ while (it.hasNext()) {
|
|
+ Entry<Node> entry = it.next();
|
|
+ Node node = entry.getValue();
|
|
+
|
|
+ node.invalid = true;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Look for wires at and around the given position that are in an invalid state
|
|
+ * and require power changes. These wires are called 'roots' because it is only
|
|
+ * when these wires change power level that neighboring wires must adjust as
|
|
+ * well.
|
|
+ *
|
|
+ * <p>
|
|
+ * While it is strictly only necessary to check the wire at the given position,
|
|
+ * if that wire is part of a network, it is beneficial to check its surroundings
|
|
+ * for other wires that require power changes. This is because a network can
|
|
+ * receive power at multiple points. Consider the following setup:
|
|
+ *
|
|
+ * <p>
|
|
+ * (top-down view, W = wire, L = lever, _ = air/other)
|
|
+ * <br> {@code _ _ W _ _ }
|
|
+ * <br> {@code _ W W W _ }
|
|
+ * <br> {@code W W L W W }
|
|
+ * <br> {@code _ W W W _ }
|
|
+ * <br> {@code _ _ W _ _ }
|
|
+ *
|
|
+ * <p>
|
|
+ * The lever powers four wires in the network at once. If this is identified
|
|
+ * correctly, the entire network can (un)power at once. While it is not
|
|
+ * practical to cover every possible situation where a network is (un)powered
|
|
+ * from multiple points at once, checking for common cases like the one
|
|
+ * described above is relatively straight-forward.
|
|
+ */
|
|
+ private void findRoots(BlockPos pos) {
|
|
+ Node node = getOrAddNode(pos);
|
|
+
|
|
+ if (!node.isWire()) {
|
|
+ return; // we should never get here
|
|
+ }
|
|
+
|
|
+ WireNode wire = node.asWire();
|
|
+ findRoot(wire);
|
|
+
|
|
+ // If the wire at the given position is not in an invalid state
|
|
+ // we can exit early.
|
|
+ if (!wire.searched) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ for (int iDir : FULL_UPDATE_ORDERS[wire.iFlowDir]) {
|
|
+ Node neighbor = getNeighbor(wire, iDir);
|
|
+
|
|
+ if (neighbor.isConductor() || neighbor.isSignalSource()) {
|
|
+ findRootsAround(neighbor, Directions.iOpposite(iDir));
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Look for wires around the given node that require power changes.
|
|
+ */
|
|
+ private void findRootsAround(Node node, int except) {
|
|
+ for (int iDir : Directions.I_EXCEPT_CARDINAL[except]) {
|
|
+ Node neighbor = getNeighbor(node, iDir);
|
|
+
|
|
+ if (neighbor.isWire()) {
|
|
+ findRoot(neighbor.asWire());
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Check if the given wire requires power changes. If it does, queue it for the
|
|
+ * breadth-first search as a root.
|
|
+ */
|
|
+ private void findRoot(WireNode wire) {
|
|
+ // Each wire only needs to be checked once.
|
|
+ if (wire.discovered) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ discover(wire);
|
|
+ findExternalPower(wire);
|
|
+ findPower(wire, false);
|
|
+
|
|
+ if (needsUpdate(wire)) {
|
|
+ searchRoot(wire);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Prepare the given wire for the breadth-first search. This means:
|
|
+ * <br>
|
|
+ * - Check if the wire should break. Rather than breaking the wire right away,
|
|
+ * its effects are integrated into the power calculations.
|
|
+ * <br>
|
|
+ * - Reset the virtual and external power.
|
|
+ * <br>
|
|
+ * - Find connections to neighboring wires.
|
|
+ */
|
|
+ private void discover(WireNode wire) {
|
|
+ if (wire.discovered) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ wire.discovered = true;
|
|
+ wire.searched = false;
|
|
+
|
|
+ if (!wire.removed && !wire.shouldBreak && !wire.state.canSurvive(level, wire.pos)) {
|
|
+ wire.shouldBreak = true;
|
|
+ }
|
|
+
|
|
+ wire.virtualPower = wire.currentPower;
|
|
+ wire.externalPower = POWER_MIN - 1;
|
|
+
|
|
+ wire.connections.set(this::getNeighbor);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Determine the power level the given wire receives from the blocks around it.
|
|
+ * Power from non-wire components only needs to be computed if power from
|
|
+ * neighboring wires has decreased, so as to determine how low the power of the
|
|
+ * wire can fall.
|
|
+ */
|
|
+ private void findPower(WireNode wire, boolean ignoreSearched) {
|
|
+ // As wire power is (re-)computed, flow information must be reset.
|
|
+ wire.virtualPower = wire.externalPower;
|
|
+ wire.flowIn = 0;
|
|
+
|
|
+ // If the wire is removed or going to break, its power level should always be
|
|
+ // the minimum value. This is because it (effectively) no longer exists, so
|
|
+ // cannot provide any power to neighboring wires.
|
|
+ if (wire.removed || wire.shouldBreak) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ // Power received from neighboring wires will never exceed POWER_MAX -
|
|
+ // POWER_STEP, so if the external power is already larger than or equal to
|
|
+ // that, there is no need to check for power from neighboring wires.
|
|
+ if (wire.externalPower < (POWER_MAX - POWER_STEP)) {
|
|
+ findWirePower(wire, ignoreSearched);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Determine the power the given wire receives from connected neighboring wires
|
|
+ * and update the virtual power accordingly.
|
|
+ */
|
|
+ private void findWirePower(WireNode wire, boolean ignoreSearched) {
|
|
+ wire.connections.forEach(connection -> {
|
|
+ if (!connection.accept) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ WireNode neighbor = connection.wire;
|
|
+
|
|
+ if (!ignoreSearched || !neighbor.searched) {
|
|
+ int power = Math.max(POWER_MIN, neighbor.virtualPower - POWER_STEP);
|
|
+ int iOpp = Directions.iOpposite(connection.iDir);
|
|
+
|
|
+ wire.offerPower(power, iOpp);
|
|
+ }
|
|
+ });
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Determine the redstone signal the given wire receives from non-wire
|
|
+ * components and update the virtual power accordingly.
|
|
+ */
|
|
+ private void findExternalPower(WireNode wire) {
|
|
+ // If the wire is removed or going to break, its power level should always be
|
|
+ // the minimum value. Thus external power need not be computed.
|
|
+ // In other cases external power need only be computed once.
|
|
+ if (wire.removed || wire.shouldBreak || wire.externalPower >= POWER_MIN) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ wire.externalPower = getExternalPower(wire);
|
|
+
|
|
+ if (wire.externalPower > wire.virtualPower) {
|
|
+ wire.virtualPower = wire.externalPower;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Determine the redstone signal the given wire receives from non-wire
|
|
+ * components.
|
|
+ */
|
|
+ private int getExternalPower(WireNode wire) {
|
|
+ int power = POWER_MIN;
|
|
+
|
|
+ for (int iDir = 0; iDir < Directions.ALL.length; iDir++) {
|
|
+ Node neighbor = getNeighbor(wire, iDir);
|
|
+
|
|
+ // Power from wires is handled separately.
|
|
+ if (neighbor.isWire()) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ // Since 1.16 there is a block that is both a conductor and a signal
|
|
+ // source: the target block!
|
|
+ if (neighbor.isConductor()) {
|
|
+ power = Math.max(power, getDirectSignalTo(wire, neighbor, Directions.iOpposite(iDir)));
|
|
+ }
|
|
+ if (neighbor.isSignalSource()) {
|
|
+ power = Math.max(power, neighbor.state.getSignal(level, neighbor.pos, Directions.ALL[iDir]));
|
|
+ }
|
|
+
|
|
+ if (power >= POWER_MAX) {
|
|
+ return POWER_MAX;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return power;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Determine the direct signal the given wire receives from neighboring blocks
|
|
+ * through the given conductor node.
|
|
+ */
|
|
+ private int getDirectSignalTo(WireNode wire, Node node, int except) {
|
|
+ int power = POWER_MIN;
|
|
+
|
|
+ for (int iDir : Directions.I_EXCEPT[except]) {
|
|
+ Node neighbor = getNeighbor(node, iDir);
|
|
+
|
|
+ if (neighbor.isSignalSource()) {
|
|
+ power = Math.max(power, neighbor.state.getDirectSignal(level, neighbor.pos, Directions.ALL[iDir]));
|
|
+
|
|
+ if (power >= POWER_MAX) {
|
|
+ return POWER_MAX;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return power;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Check if the given wire needs to update its state in the world.
|
|
+ */
|
|
+ private boolean needsUpdate(WireNode wire) {
|
|
+ return wire.removed || wire.shouldBreak || wire.virtualPower != wire.currentPower;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Queue the given wire for the breadth-first search as a root.
|
|
+ */
|
|
+ private void searchRoot(WireNode wire) {
|
|
+ int iBackupFlowDir;
|
|
+
|
|
+ if (wire.connections.iFlowDir < 0) {
|
|
+ iBackupFlowDir = 0;
|
|
+ } else {
|
|
+ iBackupFlowDir = wire.connections.iFlowDir;
|
|
+ }
|
|
+
|
|
+ search(wire, true, iBackupFlowDir);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Queue the given wire for the breadth-first search and set a backup flow
|
|
+ * direction.
|
|
+ */
|
|
+ private void search(WireNode wire, boolean root, int iBackupFlowDir) {
|
|
+ search.offer(wire);
|
|
+
|
|
+ wire.root = root;
|
|
+ wire.searched = true;
|
|
+ // Normally the flow is not set until the power level is updated. However,
|
|
+ // in networks with multiple power sources the update order between them
|
|
+ // depends on which was discovered first. To make this less prone to
|
|
+ // directionality, each wire node is given a 'backup' flow. For roots, this
|
|
+ // is the determined flow of their connections. For non-roots this is the
|
|
+ // direction from which they were discovered.
|
|
+ wire.iFlowDir = iBackupFlowDir;
|
|
+ }
|
|
+
|
|
+ private void tryUpdate() {
|
|
+ if (!search.isEmpty()) {
|
|
+ update();
|
|
+ }
|
|
+ if (!updating) {
|
|
+ nodes.clear();
|
|
+ nodeCount = 0;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Update the network and neighboring blocks. This is done in 3 steps.
|
|
+ *
|
|
+ * <p>
|
|
+ * <b>1. Search through the network</b>
|
|
+ * <br>
|
|
+ * Conduct a breadth-first search around the roots to find wires that are in an
|
|
+ * invalid state and need power changes.
|
|
+ *
|
|
+ * <p>
|
|
+ * <b>2. Depower the network</b>
|
|
+ * <br>
|
|
+ * Depower all wires in the network. This allows power to be spread most
|
|
+ * efficiently.
|
|
+ *
|
|
+ * <p>
|
|
+ * <b>3. Power the network</b>
|
|
+ * <br>
|
|
+ * Work through the update queue, setting the new power level of each wire and
|
|
+ * updating neighboring blocks. After a wire has updated its power level, it
|
|
+ * will emit shape updates and queue updates for neighboring wires and blocks.
|
|
+ */
|
|
+ private void update() {
|
|
+ // Search through the network for wires that need power changes. This includes
|
|
+ // the roots as well as any wires that will be affected by power changes to
|
|
+ // those roots.
|
|
+ searchNetwork();
|
|
+
|
|
+ // Depower all the wires in the network.
|
|
+ depowerNetwork();
|
|
+
|
|
+ // Bring each wire up to its new power level and update neighboring blocks.
|
|
+ try {
|
|
+ powerNetwork();
|
|
+ } catch (Throwable t) {
|
|
+ // If anything goes wrong while carrying out power changes, this field must
|
|
+ // be reset to 'false', or the wire handler will be locked out of carrying
|
|
+ // out power changes until the world is reloaded.
|
|
+ updating = false;
|
|
+
|
|
+ throw t;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Search through the network for wires that are in an invalid state and need
|
|
+ * power changes. These wires are added to the end of the queue, so that their
|
|
+ * neighbors can be searched next.
|
|
+ */
|
|
+ private void searchNetwork() {
|
|
+ for (WireNode wire : search) {
|
|
+ // The order in which wires are searched will influence the order in
|
|
+ // which they update their power levels.
|
|
+ wire.connections.forEach(connection -> {
|
|
+ if (!connection.offer) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ WireNode neighbor = connection.wire;
|
|
+
|
|
+ if (neighbor.searched) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ discover(neighbor);
|
|
+ findPower(neighbor, false);
|
|
+
|
|
+ // If power from neighboring wires has decreased, check for power
|
|
+ // from non-wire components so as to determine how low power can
|
|
+ // fall.
|
|
+ if (neighbor.virtualPower < neighbor.currentPower) {
|
|
+ findExternalPower(neighbor);
|
|
+ }
|
|
+
|
|
+ if (needsUpdate(neighbor)) {
|
|
+ search(neighbor, false, connection.iDir);
|
|
+ }
|
|
+ }, wire.iFlowDir);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Depower all wires in the network so that power can be spread from the power
|
|
+ * sources.
|
|
+ */
|
|
+ private void depowerNetwork() {
|
|
+ while (!search.isEmpty()) {
|
|
+ WireNode wire = search.poll();
|
|
+ findPower(wire, true);
|
|
+
|
|
+ if (wire.root || wire.removed || wire.shouldBreak || wire.virtualPower > POWER_MIN) {
|
|
+ queueWire(wire);
|
|
+ } else {
|
|
+ // Wires that do not receive any power do not queue power changes
|
|
+ // until they are offered power from a neighboring wire. To ensure
|
|
+ // that they accept any power from neighboring wires and thus queue
|
|
+ // their power changes, their virtual power is set to below the
|
|
+ // minimum.
|
|
+ wire.virtualPower--;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Work through the update queue, setting the new power level of each wire, then
|
|
+ * queueing updates to connected wires and neighboring blocks.
|
|
+ */
|
|
+ private void powerNetwork() {
|
|
+ // If an instantaneous update chain causes updates to another network
|
|
+ // (or the same network in another place), new power changes will be
|
|
+ // integrated into the already ongoing power queue, so we can exit early
|
|
+ // here.
|
|
+ if (updating) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ updating = true;
|
|
+
|
|
+ while (!updates.isEmpty()) {
|
|
+ Node node = updates.poll();
|
|
+
|
|
+ if (node.isWire()) {
|
|
+ WireNode wire = node.asWire();
|
|
+
|
|
+ if (!needsUpdate(wire)) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ findPowerFlow(wire);
|
|
+ transmitPower(wire);
|
|
+
|
|
+ if (wire.setPower()) {
|
|
+ queueNeighbors(wire);
|
|
+
|
|
+ // If the wire was newly placed or removed, shape updates have
|
|
+ // already been emitted. However, unlike before 1.19, neighbor
|
|
+ // updates are now queued, so to preserve behavior parity with
|
|
+ // previous versions, we emit extra shape updates here to
|
|
+ // notify neighboring observers.
|
|
+ updateNeighborShapes(wire);
|
|
+ }
|
|
+ } else {
|
|
+ WireNode neighborWire = node.neighborWire;
|
|
+
|
|
+ if (neighborWire != null) {
|
|
+ BlockPos neighborPos = neighborWire.pos;
|
|
+ Block neighborBlock = neighborWire.state.getBlock();
|
|
+
|
|
+ updateBlock(node, neighborPos, neighborBlock);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ updating = false;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Use the information of incoming power flow to determine the direction of
|
|
+ * power flow through this wire. If that flow is ambiguous, try to use a flow
|
|
+ * direction based on connections to neighboring wires. If that is also
|
|
+ * ambiguous, use the backup value that was set when the wire was first added to
|
|
+ * the network.
|
|
+ */
|
|
+ private void findPowerFlow(WireNode wire) {
|
|
+ int flow = FLOW_IN_TO_FLOW_OUT[wire.flowIn];
|
|
+
|
|
+ if (flow >= 0) {
|
|
+ wire.iFlowDir = flow;
|
|
+ } else if (wire.connections.iFlowDir >= 0) {
|
|
+ wire.iFlowDir = wire.connections.iFlowDir;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Transmit power from the given wire to neighboring wires and queue updates to
|
|
+ * those wires.
|
|
+ */
|
|
+ private void transmitPower(WireNode wire) {
|
|
+ wire.connections.forEach(connection -> {
|
|
+ if (!connection.offer) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ WireNode neighbor = connection.wire;
|
|
+
|
|
+ int power = Math.max(POWER_MIN, wire.virtualPower - POWER_STEP);
|
|
+ int iDir = connection.iDir;
|
|
+
|
|
+ if (neighbor.offerPower(power, iDir)) {
|
|
+ queueWire(neighbor);
|
|
+ }
|
|
+ }, wire.iFlowDir);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Emit shape updates around the given wire.
|
|
+ */
|
|
+ private void updateNeighborShapes(WireNode wire) {
|
|
+ BlockPos wirePos = wire.pos;
|
|
+ BlockState wireState = wire.state;
|
|
+
|
|
+ for (int iDir : DEFAULT_FULL_UPDATE_ORDER) {
|
|
+ Node neighbor = getNeighbor(wire, iDir);
|
|
+
|
|
+ if (!neighbor.isWire()) {
|
|
+ int iOpp = Directions.iOpposite(iDir);
|
|
+ Direction opp = Directions.ALL[iOpp];
|
|
+
|
|
+ updateShape(neighbor, opp, wirePos, wireState);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void updateShape(Node node, Direction dir, BlockPos neighborPos, BlockState neighborState) {
|
|
+ BlockPos pos = node.pos;
|
|
+ BlockState state = level.getBlockState(pos);
|
|
+
|
|
+ // Shape updates to redstone wire are very expensive, and should never happen
|
|
+ // as a result of power changes anyway.
|
|
+ if (!state.isAir() && !state.is(Blocks.REDSTONE_WIRE)) {
|
|
+ BlockState newState = state.updateShape(dir, neighborState, level, pos, neighborPos);
|
|
+ Block.updateOrDestroy(state, newState, level, pos, Block.UPDATE_CLIENTS);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Queue block updates to nodes around the given wire.
|
|
+ */
|
|
+ private void queueNeighbors(WireNode wire) {
|
|
+ forEachNeighbor(wire, neighbor -> {
|
|
+ queueNeighbor(neighbor, wire);
|
|
+ });
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Queue the given node for an update from the given neighboring wire.
|
|
+ */
|
|
+ private void queueNeighbor(Node node, WireNode neighborWire) {
|
|
+ // Updates to wires are queued when power is transmitted.
|
|
+ if (!node.isWire()) {
|
|
+ node.neighborWire = neighborWire;
|
|
+ updates.offer(node);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Queue the given wire for a power change. If the wire does not need a power
|
|
+ * change (perhaps because its power has already changed), transmit power to
|
|
+ * neighboring wires.
|
|
+ */
|
|
+ private void queueWire(WireNode wire) {
|
|
+ if (needsUpdate(wire)) {
|
|
+ updates.offer(wire);
|
|
+ } else {
|
|
+ findPowerFlow(wire);
|
|
+ transmitPower(wire);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Emit a block update to the given node.
|
|
+ */
|
|
+ private void updateBlock(Node node, BlockPos neighborPos, Block neighborBlock) {
|
|
+ BlockPos pos = node.pos;
|
|
+ BlockState state = level.getBlockState(pos);
|
|
+
|
|
+ // While this check makes sure wires in the network are not given block
|
|
+ // updates, it also prevents block updates to wires in neighboring networks.
|
|
+ // While this should not make a difference in theory, in practice, it is
|
|
+ // possible to force a network into an invalid state without updating it, even
|
|
+ // if it is relatively obscure.
|
|
+ // While I was willing to make this compromise in return for some significant
|
|
+ // performance gains in certain setups, if you are not, you can add all the
|
|
+ // positions of the network to a set and filter out block updates to wires in
|
|
+ // the network that way.
|
|
+ if (!state.isAir() && !state.is(Blocks.REDSTONE_WIRE)) {
|
|
+ state.neighborChanged(level, pos, neighborBlock, neighborPos, false);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @FunctionalInterface
|
|
+ public static interface NodeProvider {
|
|
+
|
|
+ public Node getNeighbor(Node node, int iDir);
|
|
+
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/alternate/current/wire/WireNode.java b/src/main/java/alternate/current/wire/WireNode.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..33cd90c30c22200a4e1ae64f40a0bf7864546b33
|
|
--- /dev/null
|
|
+++ b/src/main/java/alternate/current/wire/WireNode.java
|
|
@@ -0,0 +1,122 @@
|
|
+package alternate.current.wire;
|
|
+
|
|
+import net.minecraft.core.BlockPos;
|
|
+import net.minecraft.server.level.ServerLevel;
|
|
+import net.minecraft.util.Mth;
|
|
+import net.minecraft.world.level.block.Block;
|
|
+import net.minecraft.world.level.block.Blocks;
|
|
+import net.minecraft.world.level.block.RedStoneWireBlock;
|
|
+import net.minecraft.world.level.block.state.BlockState;
|
|
+import net.minecraft.world.level.redstone.Redstone;
|
|
+
|
|
+/**
|
|
+ * A WireNode is a Node that represents a wire in the world. It stores all the
|
|
+ * information about the wire that the WireHandler needs to calculate power
|
|
+ * changes.
|
|
+ *
|
|
+ * @author Space Walker
|
|
+ */
|
|
+public class WireNode extends Node {
|
|
+
|
|
+ final WireConnectionManager connections;
|
|
+
|
|
+ /** The power level this wire currently holds in the world. */
|
|
+ int currentPower;
|
|
+ /**
|
|
+ * While calculating power changes for a network, this field is used to keep
|
|
+ * track of the power level this wire should have.
|
|
+ */
|
|
+ int virtualPower;
|
|
+ /** The power level received from non-wire components. */
|
|
+ int externalPower;
|
|
+ /**
|
|
+ * A 4-bit number that keeps track of the power flow of the wires that give this
|
|
+ * wire its power level.
|
|
+ */
|
|
+ int flowIn;
|
|
+ /** The direction of power flow, based on the incoming flow. */
|
|
+ int iFlowDir;
|
|
+ boolean added;
|
|
+ boolean removed;
|
|
+ boolean shouldBreak;
|
|
+ boolean root;
|
|
+ boolean discovered;
|
|
+ boolean searched;
|
|
+
|
|
+ /** The next wire in the simple queue. */
|
|
+ WireNode next_wire;
|
|
+
|
|
+ WireNode(ServerLevel level, BlockPos pos, BlockState state) {
|
|
+ super(level);
|
|
+
|
|
+ this.pos = pos.immutable();
|
|
+ this.state = state;
|
|
+
|
|
+ this.connections = new WireConnectionManager(this);
|
|
+
|
|
+ this.virtualPower = this.currentPower = this.state.getValue(RedStoneWireBlock.POWER);
|
|
+ this.priority = priority();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ Node set(BlockPos pos, BlockState state, boolean clearNeighbors) {
|
|
+ throw new UnsupportedOperationException("Cannot update a WireNode!");
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ int priority() {
|
|
+ return Mth.clamp(virtualPower, Redstone.SIGNAL_MIN, Redstone.SIGNAL_MAX);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean isWire() {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public WireNode asWire() {
|
|
+ return this;
|
|
+ }
|
|
+
|
|
+ boolean offerPower(int power, int iDir) {
|
|
+ if (removed || shouldBreak) {
|
|
+ return false;
|
|
+ }
|
|
+ if (power == virtualPower) {
|
|
+ flowIn |= (1 << iDir);
|
|
+ return false;
|
|
+ }
|
|
+ if (power > virtualPower) {
|
|
+ virtualPower = power;
|
|
+ flowIn = (1 << iDir);
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ boolean setPower() {
|
|
+ if (removed) {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ state = level.getBlockState(pos);
|
|
+
|
|
+ if (!state.is(Blocks.REDSTONE_WIRE)) {
|
|
+ return false; // we should never get here
|
|
+ }
|
|
+
|
|
+ if (shouldBreak) {
|
|
+ Block.dropResources(state, level, pos);
|
|
+ level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_CLIENTS);
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ currentPower = LevelHelper.doRedstoneEvent(level, pos, currentPower, Mth.clamp(virtualPower, Redstone.SIGNAL_MIN, Redstone.SIGNAL_MAX));
|
|
+ state = state.setValue(RedStoneWireBlock.POWER, currentPower);
|
|
+
|
|
+ return LevelHelper.setWireState(level, pos, state, added);
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
|
|
index a96aa3568453aa2da8b5cd5b269fc0b3fec13938..4cf7e66dfee34a29894f53d17e7e13c46fb4b85e 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
|
|
@@ -224,6 +224,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
public final UUID uuid;
|
|
public boolean hasPhysicsEvent = true; // Paper
|
|
public boolean hasEntityMoveEvent = false; // Paper
|
|
+ private final alternate.current.wire.WireHandler wireHandler = new alternate.current.wire.WireHandler(this); // Paper - optimize redstone (Alternate Current)
|
|
public static Throwable getAddToWorldStackTrace(Entity entity) {
|
|
final Throwable thr = new Throwable(entity + " Added to world at " + new java.util.Date());
|
|
io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateThrowable(thr);
|
|
@@ -2590,6 +2591,13 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
return crashreportsystemdetails;
|
|
}
|
|
|
|
+ // Paper start - optimize redstone (Alternate Current)
|
|
+ @Override
|
|
+ public alternate.current.wire.WireHandler getWireHandler() {
|
|
+ return wireHandler;
|
|
+ }
|
|
+ // Paper end
|
|
+
|
|
private final class EntityCallbacks implements LevelCallback<Entity> {
|
|
|
|
EntityCallbacks() {}
|
|
diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
|
|
index 78fd63052f1eb5a650befecdad07cbc755658632..3a5bf6937a98d60be0d0142a15cf12d8de9b687f 100644
|
|
--- a/src/main/java/net/minecraft/world/level/Level.java
|
|
+++ b/src/main/java/net/minecraft/world/level/Level.java
|
|
@@ -1876,4 +1876,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
return ret;
|
|
}
|
|
// Paper end
|
|
+ // Paper start - optimize redstone (Alternate Current)
|
|
+ public alternate.current.wire.WireHandler getWireHandler() {
|
|
+ // This method is overridden in ServerLevel.
|
|
+ // Since Paper is a server platform there is no risk
|
|
+ // of this implementation being called. It is here
|
|
+ // only so this method can be called without casting
|
|
+ // an instance of Level to ServerLevel.
|
|
+ return null;
|
|
+ }
|
|
+ // Paper end - optimize redstone (Alternate Current)
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java
|
|
index 5ffb1453bf332e8035abacc97ab36d78e1248484..507be06ad51b7a212e28d3ca6680e0e4b00f4233 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java
|
|
@@ -259,7 +259,7 @@ public class RedStoneWireBlock extends Block {
|
|
return floor.isFaceSturdy(world, pos, Direction.UP) || floor.is(Blocks.HOPPER);
|
|
}
|
|
|
|
- // Paper start - Optimize redstone
|
|
+ // Paper start - Optimize redstone (Eigencraft)
|
|
// The bulk of the new functionality is found in RedstoneWireTurbo.java
|
|
com.destroystokyo.paper.util.RedstoneWireTurbo turbo = new com.destroystokyo.paper.util.RedstoneWireTurbo(this);
|
|
|
|
@@ -461,7 +461,13 @@ public class RedStoneWireBlock extends Block {
|
|
@Override
|
|
public void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) {
|
|
if (!oldState.is(state.getBlock()) && !world.isClientSide) {
|
|
- this.updateSurroundingRedstone(world, pos, state, null); // Paper - Optimize redstone
|
|
+ // Paper start - optimize redstone - replace call to updatePowerStrength
|
|
+ if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) {
|
|
+ world.getWireHandler().onWireAdded(pos); // Alternate Current
|
|
+ } else {
|
|
+ this.updateSurroundingRedstone(world, pos, state, null); // vanilla/Eigencraft
|
|
+ }
|
|
+ // Paper end
|
|
Iterator iterator = Direction.Plane.VERTICAL.iterator();
|
|
|
|
while (iterator.hasNext()) {
|
|
@@ -488,7 +494,13 @@ public class RedStoneWireBlock extends Block {
|
|
world.updateNeighborsAt(pos.relative(enumdirection), this);
|
|
}
|
|
|
|
- this.updateSurroundingRedstone(world, pos, state, null); // Paper - Optimize redstone
|
|
+ // Paper start - optimize redstone - replace call to updatePowerStrength
|
|
+ if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) {
|
|
+ world.getWireHandler().onWireRemoved(pos, state); // Alternate Current
|
|
+ } else {
|
|
+ this.updateSurroundingRedstone(world, pos, state, null); // vanilla/Eigencraft
|
|
+ }
|
|
+ // Paper end
|
|
this.updateNeighborsOfNeighboringWires(world, pos);
|
|
}
|
|
}
|
|
@@ -522,8 +534,14 @@ public class RedStoneWireBlock extends Block {
|
|
@Override
|
|
public void neighborChanged(BlockState state, Level world, BlockPos pos, Block sourceBlock, BlockPos sourcePos, boolean notify) {
|
|
if (!world.isClientSide) {
|
|
+ // Paper start - optimize redstone (Alternate Current)
|
|
+ // Alternate Current handles breaking of redstone wires in the WireHandler.
|
|
+ if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) {
|
|
+ world.getWireHandler().onWireUpdated(pos);
|
|
+ } else
|
|
+ // Paper end
|
|
if (state.canSurvive(world, pos)) {
|
|
- this.updateSurroundingRedstone(world, pos, state, sourcePos); // Paper - Optimize redstone
|
|
+ this.updateSurroundingRedstone(world, pos, state, sourcePos); // Paper - Optimize redstone (Eigencraft)
|
|
} else {
|
|
dropResources(state, world, pos);
|
|
world.removeBlock(pos, false);
|